From 783f05172b5361d96a1fff0c73dc0fa94b243ab2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Oct 2023 15:40:23 -0600 Subject: [PATCH 01/40] Make sure guests join as guests --- crates/collab/src/db/queries/channels.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index ee989b2ea0..d64d97f2ad 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -135,8 +135,7 @@ impl Database { .most_public_ancestor_for_channel(channel_id, &*tx) .await? .unwrap_or(channel_id); - // TODO: change this back to Guest. - role = Some(ChannelRole::Member); + role = Some(ChannelRole::Guest); joined_channel_id = Some(channel_id_to_join); channel_member::Entity::insert(channel_member::ActiveModel { @@ -144,8 +143,7 @@ impl Database { channel_id: ActiveValue::Set(channel_id_to_join), user_id: ActiveValue::Set(user_id), accepted: ActiveValue::Set(true), - // TODO: change this back to Guest. - role: ActiveValue::Set(ChannelRole::Member), + role: ActiveValue::Set(ChannelRole::Guest), }) .exec(&*tx) .await?; From 72ed8a6dd2ae5f2b54a40433ab7fb03d27c07a13 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Oct 2023 19:03:02 -0600 Subject: [PATCH 02/40] Allow guests to chat --- crates/collab/src/db/queries/messages.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index 06e954103d..de7334425f 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -9,7 +9,7 @@ impl Database { user_id: UserId, ) -> Result<()> { self.transaction(|tx| async move { - self.check_user_is_channel_member(channel_id, user_id, &*tx) + self.check_user_is_channel_participant(channel_id, user_id, &*tx) .await?; channel_chat_participant::ActiveModel { id: ActiveValue::NotSet, @@ -77,7 +77,7 @@ impl Database { before_message_id: Option, ) -> Result> { self.transaction(|tx| async move { - self.check_user_is_channel_member(channel_id, user_id, &*tx) + self.check_user_is_channel_participant(channel_id, user_id, &*tx) .await?; let mut condition = @@ -125,6 +125,9 @@ impl Database { nonce: u128, ) -> Result<(MessageId, Vec, Vec)> { self.transaction(|tx| async move { + self.check_user_is_channel_participant(channel_id, user_id, &*tx) + .await?; + let mut rows = channel_chat_participant::Entity::find() .filter(channel_chat_participant::Column::ChannelId.eq(channel_id)) .stream(&*tx) From 70aed4a60571baf321afc842f845e3843f9eed7b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Oct 2023 22:48:44 -0600 Subject: [PATCH 03/40] Sync Role as part of channels Begin to fix guest notifications --- crates/channel/src/channel_store.rs | 38 +-- .../src/channel_store/channel_index.rs | 2 + crates/channel/src/channel_store_tests.rs | 2 +- crates/collab/src/db.rs | 2 +- crates/collab/src/db/ids.rs | 2 +- crates/collab/src/db/queries/buffers.rs | 4 +- crates/collab/src/db/queries/channels.rs | 107 ++++---- crates/collab/src/db/tests.rs | 8 +- crates/collab/src/db/tests/channel_tests.rs | 160 ++++++------ crates/collab/src/rpc.rs | 240 +++++++++++------- .../collab/src/tests/channel_buffer_tests.rs | 7 +- crates/collab/src/tests/channel_tests.rs | 5 +- crates/collab_ui/src/chat_panel.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 8 +- crates/rpc/proto/zed.proto | 2 +- 15 files changed, 323 insertions(+), 266 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 9c80dcc2b7..de1f35bef9 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -9,7 +9,7 @@ use db::RELEASE_CHANNEL; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{ - proto::{self, ChannelEdge, ChannelPermission, ChannelRole, ChannelVisibility}, + proto::{self, ChannelEdge, ChannelVisibility}, TypedEnvelope, }; use serde_derive::{Deserialize, Serialize}; @@ -30,7 +30,6 @@ pub struct ChannelStore { channel_index: ChannelIndex, channel_invitations: Vec>, channel_participants: HashMap>>, - channels_with_admin_privileges: HashSet, outgoing_invites: HashSet<(ChannelId, UserId)>, update_channels_tx: mpsc::UnboundedSender, opened_buffers: HashMap>, @@ -50,6 +49,7 @@ pub struct Channel { pub id: ChannelId, pub name: String, pub visibility: proto::ChannelVisibility, + pub role: proto::ChannelRole, pub unseen_note_version: Option<(u64, clock::Global)>, pub unseen_message_id: Option, } @@ -164,7 +164,6 @@ impl ChannelStore { channel_invitations: Vec::default(), channel_index: ChannelIndex::default(), channel_participants: Default::default(), - channels_with_admin_privileges: Default::default(), outgoing_invites: Default::default(), opened_buffers: Default::default(), opened_chats: Default::default(), @@ -419,16 +418,11 @@ impl ChannelStore { .spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) }) } - pub fn is_user_admin(&self, channel_id: ChannelId) -> bool { - self.channel_index.iter().any(|path| { - if let Some(ix) = path.iter().position(|id| *id == channel_id) { - path[..=ix] - .iter() - .any(|id| self.channels_with_admin_privileges.contains(id)) - } else { - false - } - }) + pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool { + let Some(channel) = self.channel_for_id(channel_id) else { + return false; + }; + channel.role == proto::ChannelRole::Admin } pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc] { @@ -469,10 +463,6 @@ impl ChannelStore { proto::UpdateChannels { channels: vec![channel], insert_edge: parent_edge, - channel_permissions: vec![ChannelPermission { - channel_id, - role: ChannelRole::Admin.into(), - }], ..Default::default() }, cx, @@ -881,7 +871,6 @@ impl ChannelStore { self.channel_index.clear(); self.channel_invitations.clear(); self.channel_participants.clear(); - self.channels_with_admin_privileges.clear(); self.channel_index.clear(); self.outgoing_invites.clear(); cx.notify(); @@ -927,6 +916,7 @@ impl ChannelStore { Arc::new(Channel { id: channel.id, visibility: channel.visibility(), + role: channel.role(), name: channel.name, unseen_note_version: None, unseen_message_id: None, @@ -947,8 +937,6 @@ impl ChannelStore { self.channel_index.delete_channels(&payload.delete_channels); self.channel_participants .retain(|channel_id, _| !payload.delete_channels.contains(channel_id)); - self.channels_with_admin_privileges - .retain(|channel_id| !payload.delete_channels.contains(channel_id)); for channel_id in &payload.delete_channels { let channel_id = *channel_id; @@ -992,16 +980,6 @@ impl ChannelStore { } } - for permission in payload.channel_permissions { - if permission.role() == proto::ChannelRole::Admin { - self.channels_with_admin_privileges - .insert(permission.channel_id); - } else { - self.channels_with_admin_privileges - .remove(&permission.channel_id); - } - } - cx.notify(); if payload.channel_participants.is_empty() { return None; diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 36379a3942..54de15974e 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -125,6 +125,7 @@ impl<'a> ChannelPathsInsertGuard<'a> { if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { let existing_channel = Arc::make_mut(existing_channel); existing_channel.visibility = channel_proto.visibility(); + existing_channel.role = channel_proto.role(); existing_channel.name = channel_proto.name; } else { self.channels_by_id.insert( @@ -132,6 +133,7 @@ impl<'a> ChannelPathsInsertGuard<'a> { Arc::new(Channel { id: channel_proto.id, visibility: channel_proto.visibility(), + role: channel_proto.role(), name: channel_proto.name, unseen_note_version: None, unseen_message_id: None, diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 23f2e11a03..6a9560f608 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -376,7 +376,7 @@ fn assert_channels( ( depth, channel.name.to_string(), - store.is_user_admin(channel.id), + store.is_channel_admin(channel.id), ) }) .collect::>() diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 08f78c685d..32cf5cb20a 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -433,13 +433,13 @@ pub struct Channel { pub id: ChannelId, pub name: String, pub visibility: ChannelVisibility, + pub role: ChannelRole, } #[derive(Debug, PartialEq)] pub struct ChannelsForUser { pub channels: ChannelGraph, pub channel_participants: HashMap>, - pub channels_with_admin_privileges: HashSet, pub unseen_buffer_changes: Vec, pub channel_messages: Vec, } diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index f0de4c255e..55aae7ed3b 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -82,7 +82,7 @@ id_type!(UserId); id_type!(ChannelBufferCollaboratorId); id_type!(FlagId); -#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default)] +#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)] #[sea_orm(rs_type = "String", db_type = "String(None)")] pub enum ChannelRole { #[sea_orm(string_value = "admin")] diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 69f100e6b8..1b8467c75a 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -16,7 +16,7 @@ impl Database { connection: ConnectionId, ) -> Result { self.transaction(|tx| async move { - self.check_user_is_channel_member(channel_id, user_id, &tx) + self.check_user_is_channel_participant(channel_id, user_id, &tx) .await?; let buffer = channel::Model { @@ -131,7 +131,7 @@ impl Database { 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) + .check_user_is_channel_participant(channel_id, user_id, &*tx) .await .is_err() { diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index d64d97f2ad..65393e07f8 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -132,9 +132,12 @@ impl Database { && channel.as_ref().map(|c| c.visibility) == Some(ChannelVisibility::Public) { let channel_id_to_join = self - .most_public_ancestor_for_channel(channel_id, &*tx) + .public_path_to_channel_internal(channel_id, &*tx) .await? + .first() + .cloned() .unwrap_or(channel_id); + role = Some(ChannelRole::Guest); joined_channel_id = Some(channel_id_to_join); @@ -306,7 +309,8 @@ impl Database { self.transaction(move |tx| async move { let new_name = Self::sanitize_channel_name(new_name)?.to_string(); - self.check_user_is_channel_admin(channel_id, user_id, &*tx) + let role = self + .check_user_is_channel_admin(channel_id, user_id, &*tx) .await?; let channel = channel::ActiveModel { @@ -321,6 +325,7 @@ impl Database { id: channel.id, name: channel.name, visibility: channel.visibility, + role, }) }) .await @@ -398,6 +403,8 @@ impl Database { pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result> { self.transaction(|tx| async move { + let mut role_for_channel: HashMap = HashMap::default(); + let channel_invites = channel_member::Entity::find() .filter( channel_member::Column::UserId @@ -407,14 +414,12 @@ impl Database { .all(&*tx) .await?; + for invite in channel_invites { + role_for_channel.insert(invite.channel_id, invite.role); + } + let channels = channel::Entity::find() - .filter( - channel::Column::Id.is_in( - channel_invites - .into_iter() - .map(|channel_member| channel_member.channel_id), - ), - ) + .filter(channel::Column::Id.is_in(role_for_channel.keys().cloned())) .all(&*tx) .await?; @@ -424,6 +429,7 @@ impl Database { id: channel.id, name: channel.name, visibility: channel.visibility, + role: role_for_channel[&channel.id], }) .collect(); @@ -458,19 +464,22 @@ impl Database { ) -> Result { self.transaction(|tx| async move { let tx = tx; - - let channel_membership = channel_member::Entity::find() - .filter( - channel_member::Column::UserId - .eq(user_id) - .and(channel_member::Column::ChannelId.eq(channel_id)) - .and(channel_member::Column::Accepted.eq(true)), - ) - .all(&*tx) + let role = self + .check_user_is_channel_participant(channel_id, user_id, &*tx) .await?; - self.get_user_channels(user_id, channel_membership, &tx) - .await + self.get_user_channels( + user_id, + vec![channel_member::Model { + id: Default::default(), + channel_id, + user_id, + role, + accepted: true, + }], + &tx, + ) + .await }) .await } @@ -509,7 +518,6 @@ impl Database { } let mut channels: Vec = Vec::new(); - let mut channels_with_admin_privileges: HashSet = HashSet::default(); let mut channels_to_remove: HashSet = HashSet::default(); let mut rows = channel::Entity::find() @@ -532,11 +540,8 @@ impl Database { id: channel.id, name: channel.name, visibility: channel.visibility, + role: role, }); - - if role == ChannelRole::Admin { - channels_with_admin_privileges.insert(channel.id); - } } drop(rows); @@ -618,7 +623,6 @@ impl Database { Ok(ChannelsForUser { channels: ChannelGraph { channels, edges }, channel_participants, - channels_with_admin_privileges, unseen_buffer_changes: channel_buffer_changes, channel_messages: unseen_messages, }) @@ -787,9 +791,10 @@ impl Database { channel_id: ChannelId, user_id: UserId, tx: &DatabaseTransaction, - ) -> Result<()> { - match self.channel_role_for_user(channel_id, user_id, tx).await? { - Some(ChannelRole::Admin) => Ok(()), + ) -> Result { + let role = self.channel_role_for_user(channel_id, user_id, tx).await?; + match role { + Some(ChannelRole::Admin) => Ok(role.unwrap()), Some(ChannelRole::Member) | Some(ChannelRole::Banned) | Some(ChannelRole::Guest) @@ -818,10 +823,11 @@ impl Database { channel_id: ChannelId, user_id: UserId, tx: &DatabaseTransaction, - ) -> Result<()> { - match self.channel_role_for_user(channel_id, user_id, tx).await? { + ) -> Result { + let role = self.channel_role_for_user(channel_id, user_id, tx).await?; + match role { Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => { - Ok(()) + Ok(role.unwrap()) } Some(ChannelRole::Banned) | None => Err(anyhow!( "user is not a channel participant or channel does not exist" @@ -847,12 +853,26 @@ impl Database { Ok(row) } - pub async fn most_public_ancestor_for_channel( + // ordered from higher in tree to lower + // only considers one path to a channel + // includes the channel itself + pub async fn public_path_to_channel(&self, channel_id: ChannelId) -> Result> { + self.transaction(move |tx| async move { + Ok(self + .public_path_to_channel_internal(channel_id, &*tx) + .await?) + }) + .await + } + + // ordered from higher in tree to lower + // only considers one path to a channel + // includes the channel itself + pub async fn public_path_to_channel_internal( &self, channel_id: ChannelId, tx: &DatabaseTransaction, - ) -> Result> { - // Note: if there are many paths to a channel, this will return just one + ) -> Result> { let arbitary_path = channel_path::Entity::find() .filter(channel_path::Column::ChannelId.eq(channel_id)) .order_by(channel_path::Column::IdPath, sea_orm::Order::Desc) @@ -860,7 +880,7 @@ impl Database { .await?; let Some(path) = arbitary_path else { - return Ok(None); + return Ok(vec![]); }; let ancestor_ids: Vec = path @@ -882,13 +902,10 @@ impl Database { visible_channels.insert(row.id); } - for ancestor in ancestor_ids { - if visible_channels.contains(&ancestor) { - return Ok(Some(ancestor)); - } - } - - Ok(None) + Ok(ancestor_ids + .into_iter() + .filter(|id| visible_channels.contains(id)) + .collect()) } pub async fn channel_role_for_user( @@ -1059,7 +1076,8 @@ impl Database { /// Returns the channel with the given ID pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result { self.transaction(|tx| async move { - self.check_user_is_channel_participant(channel_id, user_id, &*tx) + let role = self + .check_user_is_channel_participant(channel_id, user_id, &*tx) .await?; let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?; @@ -1070,6 +1088,7 @@ impl Database { Ok(Channel { id: channel.id, visibility: channel.visibility, + role, name: channel.name, }) }) diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 99a605106e..a53509a59f 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -149,17 +149,21 @@ impl Drop for TestDb { } /// The second tuples are (channel_id, parent) -fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId)]) -> ChannelGraph { +fn graph( + channels: &[(ChannelId, &'static str, ChannelRole)], + edges: &[(ChannelId, ChannelId)], +) -> ChannelGraph { let mut graph = ChannelGraph { channels: vec![], edges: vec![], }; - for (id, name) in channels { + for (id, name, role) in channels { graph.channels.push(Channel { id: *id, name: name.to_string(), visibility: ChannelVisibility::Members, + role: *role, }) } diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 40842aff5c..a323f2919e 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -8,45 +8,20 @@ use crate::{ db::{ queries::channels::ChannelGraph, tests::{graph, TEST_RELEASE_CHANNEL}, - ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId, + ChannelId, ChannelRole, Database, NewUserParams, RoomId, ServerId, UserId, }, test_both_dbs, }; use std::sync::{ - atomic::{AtomicI32, Ordering}, + atomic::{AtomicI32, AtomicU32, Ordering}, Arc, }; test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite); async fn test_channels(db: &Arc) { - 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 b_id = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; + let a_id = new_test_user(db, "user1@example.com").await; + let b_id = new_test_user(db, "user2@example.com").await; let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); @@ -91,13 +66,13 @@ async fn test_channels(db: &Arc) { result.channels, graph( &[ - (zed_id, "zed"), - (crdb_id, "crdb"), - (livestreaming_id, "livestreaming"), - (replace_id, "replace"), - (rust_id, "rust"), - (cargo_id, "cargo"), - (cargo_ra_id, "cargo-ra") + (zed_id, "zed", ChannelRole::Admin), + (crdb_id, "crdb", ChannelRole::Admin), + (livestreaming_id, "livestreaming", ChannelRole::Admin), + (replace_id, "replace", ChannelRole::Admin), + (rust_id, "rust", ChannelRole::Admin), + (cargo_id, "cargo", ChannelRole::Admin), + (cargo_ra_id, "cargo-ra", ChannelRole::Admin) ], &[ (crdb_id, zed_id), @@ -114,10 +89,10 @@ async fn test_channels(db: &Arc) { result.channels, graph( &[ - (zed_id, "zed"), - (crdb_id, "crdb"), - (livestreaming_id, "livestreaming"), - (replace_id, "replace") + (zed_id, "zed", ChannelRole::Member), + (crdb_id, "crdb", ChannelRole::Member), + (livestreaming_id, "livestreaming", ChannelRole::Member), + (replace_id, "replace", ChannelRole::Member) ], &[ (crdb_id, zed_id), @@ -142,10 +117,10 @@ async fn test_channels(db: &Arc) { result.channels, graph( &[ - (zed_id, "zed"), - (crdb_id, "crdb"), - (livestreaming_id, "livestreaming"), - (replace_id, "replace") + (zed_id, "zed", ChannelRole::Admin), + (crdb_id, "crdb", ChannelRole::Admin), + (livestreaming_id, "livestreaming", ChannelRole::Admin), + (replace_id, "replace", ChannelRole::Admin) ], &[ (crdb_id, zed_id), @@ -179,32 +154,8 @@ test_both_dbs!( async fn test_joining_channels(db: &Arc) { let owner_id = db.create_server("test").await.unwrap().0 as u32; - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; + let user_1 = new_test_user(db, "user1@example.com").await; + let user_2 = new_test_user(db, "user2@example.com").await; let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap(); @@ -523,7 +474,11 @@ async fn test_db_channel_moving(db: &Arc) { pretty_assertions::assert_eq!( returned_channels, graph( - &[(livestreaming_dag_sub_id, "livestreaming_dag_sub")], + &[( + livestreaming_dag_sub_id, + "livestreaming_dag_sub", + ChannelRole::Admin + )], &[(livestreaming_dag_sub_id, livestreaming_id)] ) ); @@ -560,9 +515,17 @@ async fn test_db_channel_moving(db: &Arc) { returned_channels, graph( &[ - (livestreaming_id, "livestreaming"), - (livestreaming_dag_id, "livestreaming_dag"), - (livestreaming_dag_sub_id, "livestreaming_dag_sub"), + (livestreaming_id, "livestreaming", ChannelRole::Admin), + ( + livestreaming_dag_id, + "livestreaming_dag", + ChannelRole::Admin + ), + ( + livestreaming_dag_sub_id, + "livestreaming_dag_sub", + ChannelRole::Admin + ), ], &[ (livestreaming_id, gpui2_id), @@ -1080,13 +1043,46 @@ async fn test_user_joins_correct_channel(db: &Arc) { .unwrap(); let most_public = db - .transaction( - |tx| async move { db.most_public_ancestor_for_channel(vim_channel, &*tx).await }, - ) + .public_path_to_channel(vim_channel) + .await + .unwrap() + .first() + .cloned(); + + assert_eq!(most_public, Some(zed_channel)) +} + +test_both_dbs!( + test_guest_access, + test_guest_access_postgres, + test_guest_access_sqlite +); + +async fn test_guest_access(db: &Arc) { + let server = db.create_server("test").await.unwrap(); + + let admin = new_test_user(db, "admin@example.com").await; + let guest = new_test_user(db, "guest@example.com").await; + let guest_connection = new_test_connection(server); + + let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); + db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin) .await .unwrap(); - assert_eq!(most_public, Some(zed_channel)) + assert!(db + .join_channel_chat(zed_channel, guest_connection, guest) + .await + .is_err()); + + db.join_channel(zed_channel, guest, guest_connection, TEST_RELEASE_CHANNEL) + .await + .unwrap(); + + assert!(db + .join_channel_chat(zed_channel, guest_connection, guest) + .await + .is_ok()) } #[track_caller] @@ -1130,3 +1126,11 @@ async fn new_test_user(db: &Arc, email: &str) -> UserId { .unwrap() .user_id } + +static TEST_CONNECTION_ID: AtomicU32 = AtomicU32::new(1); +fn new_test_connection(server: ServerId) -> ConnectionId { + ConnectionId { + id: TEST_CONNECTION_ID.fetch_add(1, Ordering::SeqCst), + owner_id: server.0 as u32, + } +} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 15ea3b24e1..aa8a4552b3 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -3,8 +3,8 @@ mod connection_pool; use crate::{ auth, db::{ - self, BufferId, ChannelId, ChannelVisibility, ChannelsForUser, Database, MessageId, - ProjectId, RoomId, ServerId, User, UserId, + self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, + ServerId, User, UserId, }, executor::Executor, AppState, Result, @@ -2206,14 +2206,13 @@ async fn create_channel( .create_channel(&request.name, parent_id, session.user_id) .await?; - let channel = proto::Channel { - id: id.to_proto(), - name: request.name, - visibility: proto::ChannelVisibility::Members as i32, - }; - response.send(proto::CreateChannelResponse { - channel: Some(channel.clone()), + channel: Some(proto::Channel { + id: id.to_proto(), + name: request.name, + visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), + }), parent_id: request.parent_id, })?; @@ -2221,19 +2220,26 @@ async fn create_channel( return Ok(()); }; - let update = proto::UpdateChannels { - channels: vec![channel], - insert_edge: vec![ChannelEdge { - parent_id: parent_id.to_proto(), - channel_id: id.to_proto(), - }], - ..Default::default() - }; + let members: Vec = db + .get_channel_participant_details(parent_id, session.user_id) + .await? + .into_iter() + .filter(|member| { + member.role() == proto::ChannelRole::Admin + || member.role() == proto::ChannelRole::Member + }) + .collect(); - let user_ids_to_notify = db.get_channel_members(parent_id).await?; + let mut updates: HashMap = HashMap::default(); + + for member in members { + let user_id = UserId::from_proto(member.user_id); + let channels = db.get_channel_for_user(parent_id, user_id).await?; + updates.insert(user_id, build_initial_channels_update(channels, vec![])); + } let connection_pool = session.connection_pool().await; - for user_id in user_ids_to_notify { + for (user_id, update) in updates { for connection_id in connection_pool.user_connection_ids(user_id) { if user_id == session.user_id { continue; @@ -2297,6 +2303,7 @@ async fn invite_channel_member( id: channel.id.to_proto(), visibility: channel.visibility.into(), name: channel.name, + role: request.role().into(), }); for connection_id in session .connection_pool() @@ -2350,18 +2357,23 @@ async fn set_channel_visibility( .set_channel_visibility(channel_id, visibility, session.user_id) .await?; - let mut update = proto::UpdateChannels::default(); - update.channels.push(proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - visibility: channel.visibility.into(), - }); - - let member_ids = db.get_channel_members(channel_id).await?; + let members = db + .get_channel_participant_details(channel_id, session.user_id) + .await?; let connection_pool = session.connection_pool().await; - for member_id in member_ids { - for connection_id in connection_pool.user_connection_ids(member_id) { + // TODO: notify people who were guests and are now not allowed. + for member in members { + for connection_id in connection_pool.user_connection_ids(UserId::from_proto(member.user_id)) + { + let mut update = proto::UpdateChannels::default(); + update.channels.push(proto::Channel { + id: channel.id.to_proto(), + name: channel.name.clone(), + visibility: channel.visibility.into(), + role: member.role.into(), + }); + session.peer.send(connection_id, update.clone())?; } } @@ -2387,13 +2399,17 @@ async fn set_channel_member_role( ) .await?; - let channel = db.get_channel(channel_id, session.user_id).await?; - let mut update = proto::UpdateChannels::default(); if channel_member.accepted { - update.channel_permissions.push(proto::ChannelPermission { - channel_id: channel.id.to_proto(), - role: request.role, + let channels = db.get_channel_for_user(channel_id, member_id).await?; + update = build_initial_channels_update(channels, vec![]); + } else { + let channel = db.get_channel(channel_id, session.user_id).await?; + update.channel_invitations.push(proto::Channel { + id: channel_id.to_proto(), + visibility: channel.visibility.into(), + name: channel.name, + role: request.role().into(), }); } @@ -2420,22 +2436,31 @@ async fn rename_channel( .rename_channel(channel_id, session.user_id, &request.name) .await?; - let channel = proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - visibility: channel.visibility.into(), - }; response.send(proto::RenameChannelResponse { - channel: Some(channel.clone()), + channel: Some(proto::Channel { + id: channel.id.to_proto(), + name: channel.name.clone(), + visibility: channel.visibility.into(), + role: proto::ChannelRole::Admin.into(), + }), })?; - let mut update = proto::UpdateChannels::default(); - update.channels.push(channel); - let member_ids = db.get_channel_members(channel_id).await?; + let members = db + .get_channel_participant_details(channel_id, session.user_id) + .await?; let connection_pool = session.connection_pool().await; - for member_id in member_ids { - for connection_id in connection_pool.user_connection_ids(member_id) { + for member in members { + for connection_id in connection_pool.user_connection_ids(UserId::from_proto(member.user_id)) + { + let mut update = proto::UpdateChannels::default(); + update.channels.push(proto::Channel { + id: channel.id.to_proto(), + name: channel.name.clone(), + visibility: channel.visibility.into(), + role: member.role.into(), + }); + session.peer.send(connection_id, update.clone())?; } } @@ -2463,6 +2488,10 @@ async fn link_channel( id: channel.id.to_proto(), visibility: channel.visibility.into(), name: channel.name, + // TODO: not all these members should be able to see all those channels + // the channels in channels_to_send are from the admin point of view, + // but any public guests should only get updates about public channels. + role: todo!(), }) .collect(), insert_edge: channels_to_send.edges, @@ -2521,6 +2550,12 @@ async fn move_channel( let from_parent = ChannelId::from_proto(request.from); let to = ChannelId::from_proto(request.to); + let from_public_parent = db + .public_path_to_channel(from_parent) + .await? + .last() + .cloned(); + let channels_to_send = db .move_channel(session.user_id, channel_id, from_parent, to) .await?; @@ -2530,38 +2565,68 @@ async fn move_channel( return Ok(()); } - let members_from = db.get_channel_members(from_parent).await?; - let members_to = db.get_channel_members(to).await?; + let to_public_parent = db.public_path_to_channel(to).await?.last().cloned(); - let update = proto::UpdateChannels { - delete_edge: vec![proto::ChannelEdge { - channel_id: channel_id.to_proto(), - parent_id: from_parent.to_proto(), - }], - ..Default::default() - }; - let connection_pool = session.connection_pool().await; - for member_id in members_from { - for connection_id in connection_pool.user_connection_ids(member_id) { - session.peer.send(connection_id, update.clone())?; + let members_from = db + .get_channel_participant_details(from_parent, session.user_id) + .await? + .into_iter() + .filter(|member| { + member.role() == proto::ChannelRole::Admin || member.role() == proto::ChannelRole::Guest + }); + let members_to = db + .get_channel_participant_details(to, session.user_id) + .await? + .into_iter() + .filter(|member| { + member.role() == proto::ChannelRole::Admin || member.role() == proto::ChannelRole::Guest + }); + + let mut updates: HashMap = HashMap::default(); + + for member in members_to { + let user_id = UserId::from_proto(member.user_id); + let channels = db.get_channel_for_user(to, user_id).await?; + updates.insert(user_id, build_initial_channels_update(channels, vec![])); + } + + if let Some(to_public_parent) = to_public_parent { + // only notify guests of public channels (admins/members are notified by members_to above, and banned users don't care) + let public_members_to = db + .get_channel_participant_details(to, session.user_id) + .await? + .into_iter() + .filter(|member| member.role() == proto::ChannelRole::Guest); + + for member in public_members_to { + let user_id = UserId::from_proto(member.user_id); + if updates.contains_key(&user_id) { + continue; + } + let channels = db.get_channel_for_user(to_public_parent, user_id).await?; + updates.insert(user_id, build_initial_channels_update(channels, vec![])); } } - let update = proto::UpdateChannels { - channels: channels_to_send - .channels - .into_iter() - .map(|channel| proto::Channel { - id: channel.id.to_proto(), - visibility: channel.visibility.into(), - name: channel.name, - }) - .collect(), - insert_edge: channels_to_send.edges, - ..Default::default() - }; - for member_id in members_to { - for connection_id in connection_pool.user_connection_ids(member_id) { + for member in members_from { + let user_id = UserId::from_proto(member.user_id); + let update = updates + .entry(user_id) + .or_insert(proto::UpdateChannels::default()); + update.delete_edge.push(proto::ChannelEdge { + channel_id: channel_id.to_proto(), + parent_id: from_parent.to_proto(), + }) + } + + if let Some(_from_public_parent) = from_public_parent { + // TODO: for each guest member of the old public parent + // delete the edge that they could see (from the from_public_parent down) + } + + let connection_pool = session.connection_pool().await; + for (user_id, update) in updates { + for connection_id in connection_pool.user_connection_ids(user_id) { session.peer.send(connection_id, update.clone())?; } } @@ -2628,6 +2693,7 @@ async fn channel_membership_updated( .map(|channel| proto::Channel { id: channel.id.to_proto(), visibility: channel.visibility.into(), + role: channel.role.into(), name: channel.name, }), ); @@ -2645,17 +2711,6 @@ async fn channel_membership_updated( participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(), }), ); - update - .channel_permissions - .extend( - result - .channels_with_admin_privileges - .into_iter() - .map(|channel_id| proto::ChannelPermission { - channel_id: channel_id.to_proto(), - role: proto::ChannelRole::Admin.into(), - }), - ); session.peer.send(session.connection_id, update)?; Ok(()) } @@ -3149,6 +3204,7 @@ fn build_initial_channels_update( id: channel.id.to_proto(), name: channel.name, visibility: channel.visibility.into(), + role: channel.role.into(), }); } @@ -3165,24 +3221,12 @@ fn build_initial_channels_update( }); } - update - .channel_permissions - .extend( - channels - .channels_with_admin_privileges - .into_iter() - .map(|id| proto::ChannelPermission { - channel_id: id.to_proto(), - role: proto::ChannelRole::Admin.into(), - }), - ); - for channel in channel_invites { update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, - // TODO: Visibility - visibility: ChannelVisibility::Public.into(), + visibility: channel.visibility.into(), + role: channel.role.into(), }); } diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 14ae159ab8..f2ab2a9d09 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -413,7 +413,7 @@ async fn test_channel_buffer_disconnect( channel_buffer_a.update(cx_a, |buffer, _| { assert_eq!( buffer.channel().as_ref(), - &channel(channel_id, "the-channel") + &channel(channel_id, "the-channel", proto::ChannelRole::Admin) ); assert!(!buffer.is_connected()); }); @@ -438,15 +438,16 @@ async fn test_channel_buffer_disconnect( channel_buffer_b.update(cx_b, |buffer, _| { assert_eq!( buffer.channel().as_ref(), - &channel(channel_id, "the-channel") + &channel(channel_id, "the-channel", proto::ChannelRole::Member) ); assert!(!buffer.is_connected()); }); } -fn channel(id: u64, name: &'static str) -> Channel { +fn channel(id: u64, name: &'static str, role: proto::ChannelRole) -> Channel { Channel { id, + role, name: name.to_string(), visibility: proto::ChannelVisibility::Members, unseen_note_version: None, diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 1bb8c92ac8..af4b99c4ef 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -152,6 +152,7 @@ async fn test_core_channels( }, ], ); + dbg!("-------"); let channel_c_id = client_a .channel_store() @@ -1295,7 +1296,7 @@ fn assert_channel_invitations( depth: 0, name: channel.name.clone(), id: channel.id, - user_is_admin: store.is_user_admin(channel.id), + user_is_admin: store.is_channel_admin(channel.id), }) .collect::>() }); @@ -1315,7 +1316,7 @@ fn assert_channels( depth, name: channel.name.clone(), id: channel.id, - user_is_admin: store.is_user_admin(channel.id), + user_is_admin: store.is_channel_admin(channel.id), }) .collect::>() }); diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index a8c4006cb8..f0a6c96ced 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -360,7 +360,7 @@ impl ChatPanel { let is_admin = self .channel_store .read(cx) - .is_user_admin(active_chat.channel().id); + .is_channel_admin(active_chat.channel().id); let last_message = active_chat.message(ix.saturating_sub(1)); let this_message = active_chat.message(ix); let is_continuation = last_message.id != this_message.id diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 2e68a1c939..dcebe35b26 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2721,7 +2721,11 @@ impl CollabPanel { }, )); - if self.channel_store.read(cx).is_user_admin(path.channel_id()) { + if self + .channel_store + .read(cx) + .is_channel_admin(path.channel_id()) + { let parent_id = path.parent_id(); items.extend([ @@ -3160,7 +3164,7 @@ impl CollabPanel { fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext) { let channel_store = self.channel_store.read(cx); - if !channel_store.is_user_admin(action.location.channel_id()) { + if !channel_store.is_channel_admin(action.location.channel_id()) { return; } if let Some(channel) = channel_store diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 0ca10b381a..20a47954a4 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -970,7 +970,6 @@ message UpdateChannels { repeated Channel channel_invitations = 5; repeated uint64 remove_channel_invitations = 6; repeated ChannelParticipants channel_participants = 7; - repeated ChannelPermission channel_permissions = 8; repeated UnseenChannelMessage unseen_channel_messages = 9; repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10; } @@ -1568,6 +1567,7 @@ message Channel { uint64 id = 1; string name = 2; ChannelVisibility visibility = 3; + ChannelRole role = 4; } message Contact { From 0ce1ec5d15158a5ff83c4221b9d59325f032bef5 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 18 Oct 2023 05:28:05 -0700 Subject: [PATCH 04/40] Restrict DAG-related functionality, but retain infrastructure for implementing symlinks --- crates/channel/src/channel_store_tests.rs | 16 +- crates/collab/src/db/queries/channels.rs | 52 +++- crates/collab/src/rpc.rs | 125 ++++---- crates/collab/src/tests/channel_tests.rs | 345 +++++++++++----------- crates/collab_ui/src/collab_panel.rs | 188 +++--------- crates/rpc/proto/zed.proto | 6 + 6 files changed, 338 insertions(+), 394 deletions(-) diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 6a9560f608..69c0cd37fc 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -19,17 +19,15 @@ fn test_update_channels(cx: &mut AppContext) { id: 1, name: "b".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), }, proto::Channel { id: 2, name: "a".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Member.into(), }, ], - channel_permissions: vec![proto::ChannelPermission { - channel_id: 1, - role: proto::ChannelRole::Admin.into(), - }], ..Default::default() }, cx, @@ -52,11 +50,13 @@ fn test_update_channels(cx: &mut AppContext) { id: 3, name: "x".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Member.into(), }, proto::Channel { id: 4, name: "y".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Member.into(), }, ], insert_edge: vec![ @@ -97,16 +97,19 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { id: 0, name: "a".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), }, proto::Channel { id: 1, name: "b".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), }, proto::Channel { id: 2, name: "c".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), }, ], insert_edge: vec![ @@ -119,10 +122,6 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { channel_id: 2, }, ], - channel_permissions: vec![proto::ChannelPermission { - channel_id: 0, - role: proto::ChannelRole::Admin.into(), - }], ..Default::default() }, cx, @@ -166,6 +165,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { id: channel_id, name: "the-channel".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), }], ..Default::default() }); diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 65393e07f8..575f55fe02 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -419,7 +419,7 @@ impl Database { } let channels = channel::Entity::find() - .filter(channel::Column::Id.is_in(role_for_channel.keys().cloned())) + .filter(channel::Column::Id.is_in(role_for_channel.keys().copied())) .all(&*tx) .await?; @@ -633,6 +633,36 @@ impl Database { .await } + pub async fn get_channel_members_and_roles( + &self, + id: ChannelId, + ) -> Result> { + self.transaction(|tx| async move { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryUserIdsAndRoles { + UserId, + Role, + } + + let ancestor_ids = self.get_channel_ancestors(id, &*tx).await?; + let user_ids_and_roles = channel_member::Entity::find() + .distinct() + .filter( + channel_member::Column::ChannelId + .is_in(ancestor_ids.iter().copied()) + .and(channel_member::Column::Accepted.eq(true)), + ) + .select_only() + .column(channel_member::Column::UserId) + .column(channel_member::Column::Role) + .into_values::<_, QueryUserIdsAndRoles>() + .all(&*tx) + .await?; + Ok(user_ids_and_roles) + }) + .await + } + pub async fn set_channel_member_role( &self, channel_id: ChannelId, @@ -1138,9 +1168,6 @@ impl Database { to: ChannelId, ) -> Result { self.transaction(|tx| async move { - // Note that even with these maxed permissions, this linking operation - // is still insecure because you can't remove someone's permissions to a - // channel if they've linked the channel to one where they're an admin. self.check_user_is_channel_admin(channel, user, &*tx) .await?; @@ -1327,6 +1354,23 @@ impl Database { }) .await } + + pub async fn assert_root_channel(&self, channel: ChannelId) -> Result<()> { + self.transaction(|tx| async move { + let path = channel_path::Entity::find() + .filter(channel_path::Column::ChannelId.eq(channel)) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such channel found"))?; + + let mut id_parts = path.id_path.trim_matches('/').split('/'); + + (id_parts.next().is_some() && id_parts.next().is_none()) + .then_some(()) + .ok_or_else(|| anyhow!("channel is not a root channel").into()) + }) + .await + } } #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index aa8a4552b3..c7a2ba88b1 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -3,8 +3,8 @@ mod connection_pool; use crate::{ auth, db::{ - self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, - ServerId, User, UserId, + self, BufferId, ChannelId, ChannelRole, ChannelVisibility, ChannelsForUser, Database, + MessageId, ProjectId, RoomId, ServerId, User, UserId, }, executor::Executor, AppState, Result, @@ -38,8 +38,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage, - LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators, + self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, + RequestMessage, UpdateChannelBufferCollaborators, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -2366,15 +2366,18 @@ async fn set_channel_visibility( for member in members { for connection_id in connection_pool.user_connection_ids(UserId::from_proto(member.user_id)) { - let mut update = proto::UpdateChannels::default(); - update.channels.push(proto::Channel { - id: channel.id.to_proto(), - name: channel.name.clone(), - visibility: channel.visibility.into(), - role: member.role.into(), - }); - - session.peer.send(connection_id, update.clone())?; + session.peer.send( + connection_id, + proto::UpdateChannels { + channels: vec![proto::Channel { + id: channel.id.to_proto(), + name: channel.name.clone(), + visibility: channel.visibility.into(), + role: member.role.into(), + }], + ..Default::default() + }, + )?; } } @@ -2468,6 +2471,8 @@ async fn rename_channel( Ok(()) } +// TODO: Implement in terms of symlinks +// Current behavior of this is more like 'Move root channel' async fn link_channel( request: proto::LinkChannel, response: Response, @@ -2476,30 +2481,46 @@ async fn link_channel( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let to = ChannelId::from_proto(request.to); - let channels_to_send = db.link_channel(session.user_id, channel_id, to).await?; - let members = db.get_channel_members(to).await?; + // TODO: Remove this restriction once we have symlinks + db.assert_root_channel(channel_id).await?; + + let channels_to_send = db.link_channel(session.user_id, channel_id, to).await?; + let members = db.get_channel_members_and_roles(to).await?; let connection_pool = session.connection_pool().await; - let update = proto::UpdateChannels { - channels: channels_to_send - .channels - .into_iter() - .map(|channel| proto::Channel { - id: channel.id.to_proto(), - visibility: channel.visibility.into(), - name: channel.name, - // TODO: not all these members should be able to see all those channels - // the channels in channels_to_send are from the admin point of view, - // but any public guests should only get updates about public channels. - role: todo!(), - }) - .collect(), - insert_edge: channels_to_send.edges, - ..Default::default() - }; - for member_id in members { + + for (member_id, role) in members { + let build_channel_proto = |channel: &db::Channel| proto::Channel { + id: channel.id.to_proto(), + visibility: channel.visibility.into(), + name: channel.name.clone(), + role: role.into(), + }; + for connection_id in connection_pool.user_connection_ids(member_id) { - session.peer.send(connection_id, update.clone())?; + let channels: Vec<_> = if role == ChannelRole::Guest { + channels_to_send + .channels + .iter() + .filter(|channel| channel.visibility != ChannelVisibility::Public) + .map(build_channel_proto) + .collect() + } else { + channels_to_send + .channels + .iter() + .map(build_channel_proto) + .collect() + }; + + session.peer.send( + connection_id, + proto::UpdateChannels { + channels, + insert_edge: channels_to_send.edges.clone(), + ..Default::default() + }, + )?; } } @@ -2508,36 +2529,13 @@ async fn link_channel( Ok(()) } +// TODO: Implement in terms of symlinks async fn unlink_channel( - request: proto::UnlinkChannel, - response: Response, - session: Session, + _request: proto::UnlinkChannel, + _response: Response, + _session: Session, ) -> Result<()> { - let db = session.db().await; - let channel_id = ChannelId::from_proto(request.channel_id); - let from = ChannelId::from_proto(request.from); - - db.unlink_channel(session.user_id, channel_id, from).await?; - - let members = db.get_channel_members(from).await?; - - let update = proto::UpdateChannels { - delete_edge: vec![proto::ChannelEdge { - channel_id: channel_id.to_proto(), - parent_id: from.to_proto(), - }], - ..Default::default() - }; - let connection_pool = session.connection_pool().await; - for member_id in members { - for connection_id in connection_pool.user_connection_ids(member_id) { - session.peer.send(connection_id, update.clone())?; - } - } - - response.send(Ack {})?; - - Ok(()) + Err(anyhow!("unimplemented").into()) } async fn move_channel( @@ -2554,7 +2552,7 @@ async fn move_channel( .public_path_to_channel(from_parent) .await? .last() - .cloned(); + .copied(); let channels_to_send = db .move_channel(session.user_id, channel_id, from_parent, to) @@ -2574,6 +2572,7 @@ async fn move_channel( .filter(|member| { member.role() == proto::ChannelRole::Admin || member.role() == proto::ChannelRole::Guest }); + let members_to = db .get_channel_participant_details(to, session.user_id) .await? diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index af4b99c4ef..f982f05ee3 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1031,14 +1031,14 @@ async fn test_invite_access( async fn test_channel_moving( deterministic: Arc, cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &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 client_b = server.create_client(cx_b, "user_b").await; + // let client_c = server.create_client(cx_c, "user_c").await; let channels = server .make_channel_tree( @@ -1091,187 +1091,188 @@ async fn test_channel_moving( ], ); - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.link_channel(channel_d_id, channel_c_id, cx) - }) - .await - .unwrap(); + // TODO: Restore this test once we have a way to make channel symlinks + // client_a + // .channel_store() + // .update(cx_a, |channel_store, cx| { + // channel_store.link_channel(channel_d_id, channel_c_id, cx) + // }) + // .await + // .unwrap(); - // Current shape for A: - // /------\ - // a - b -- c -- d - assert_channels_list_shape( - client_a.channel_store(), - cx_a, - &[ - (channel_a_id, 0), - (channel_b_id, 1), - (channel_c_id, 2), - (channel_d_id, 3), - (channel_d_id, 2), - ], - ); + // // Current shape for A: + // // /------\ + // // a - b -- c -- d + // assert_channels_list_shape( + // client_a.channel_store(), + // cx_a, + // &[ + // (channel_a_id, 0), + // (channel_b_id, 1), + // (channel_c_id, 2), + // (channel_d_id, 3), + // (channel_d_id, 2), + // ], + // ); + // + // let b_channels = server + // .make_channel_tree( + // &[ + // ("channel-mu", None), + // ("channel-gamma", Some("channel-mu")), + // ("channel-epsilon", Some("channel-mu")), + // ], + // (&client_b, cx_b), + // ) + // .await; + // let channel_mu_id = b_channels[0]; + // let channel_ga_id = b_channels[1]; + // let channel_ep_id = b_channels[2]; - let b_channels = server - .make_channel_tree( - &[ - ("channel-mu", None), - ("channel-gamma", Some("channel-mu")), - ("channel-epsilon", Some("channel-mu")), - ], - (&client_b, cx_b), - ) - .await; - let channel_mu_id = b_channels[0]; - let channel_ga_id = b_channels[1]; - let channel_ep_id = b_channels[2]; + // // Current shape for B: + // // /- ep + // // mu -- ga + // assert_channels_list_shape( + // client_b.channel_store(), + // cx_b, + // &[(channel_mu_id, 0), (channel_ep_id, 1), (channel_ga_id, 1)], + // ); - // Current shape for B: - // /- ep - // mu -- ga - assert_channels_list_shape( - client_b.channel_store(), - cx_b, - &[(channel_mu_id, 0), (channel_ep_id, 1), (channel_ga_id, 1)], - ); + // client_a + // .add_admin_to_channel((&client_b, cx_b), channel_b_id, cx_a) + // .await; - client_a - .add_admin_to_channel((&client_b, cx_b), channel_b_id, cx_a) - .await; + // // Current shape for B: + // // /- ep + // // mu -- ga + // // /---------\ + // // b -- c -- d + // assert_channels_list_shape( + // client_b.channel_store(), + // cx_b, + // &[ + // // New channels from a + // (channel_b_id, 0), + // (channel_c_id, 1), + // (channel_d_id, 2), + // (channel_d_id, 1), + // // B's old channels + // (channel_mu_id, 0), + // (channel_ep_id, 1), + // (channel_ga_id, 1), + // ], + // ); - // Current shape for B: - // /- ep - // mu -- ga - // /---------\ - // b -- c -- d - assert_channels_list_shape( - client_b.channel_store(), - cx_b, - &[ - // New channels from a - (channel_b_id, 0), - (channel_c_id, 1), - (channel_d_id, 2), - (channel_d_id, 1), - // B's old channels - (channel_mu_id, 0), - (channel_ep_id, 1), - (channel_ga_id, 1), - ], - ); + // client_b + // .add_admin_to_channel((&client_c, cx_c), channel_ep_id, cx_b) + // .await; - client_b - .add_admin_to_channel((&client_c, cx_c), channel_ep_id, cx_b) - .await; + // // Current shape for C: + // // - ep + // assert_channels_list_shape(client_c.channel_store(), cx_c, &[(channel_ep_id, 0)]); - // Current shape for C: - // - ep - assert_channels_list_shape(client_c.channel_store(), cx_c, &[(channel_ep_id, 0)]); + // client_b + // .channel_store() + // .update(cx_b, |channel_store, cx| { + // channel_store.link_channel(channel_b_id, channel_ep_id, cx) + // }) + // .await + // .unwrap(); - client_b - .channel_store() - .update(cx_b, |channel_store, cx| { - channel_store.link_channel(channel_b_id, channel_ep_id, cx) - }) - .await - .unwrap(); + // // Current shape for B: + // // /---------\ + // // /- ep -- b -- c -- d + // // mu -- ga + // assert_channels_list_shape( + // client_b.channel_store(), + // cx_b, + // &[ + // (channel_mu_id, 0), + // (channel_ep_id, 1), + // (channel_b_id, 2), + // (channel_c_id, 3), + // (channel_d_id, 4), + // (channel_d_id, 3), + // (channel_ga_id, 1), + // ], + // ); - // Current shape for B: - // /---------\ - // /- ep -- b -- c -- d - // mu -- ga - assert_channels_list_shape( - client_b.channel_store(), - cx_b, - &[ - (channel_mu_id, 0), - (channel_ep_id, 1), - (channel_b_id, 2), - (channel_c_id, 3), - (channel_d_id, 4), - (channel_d_id, 3), - (channel_ga_id, 1), - ], - ); + // // Current shape for C: + // // /---------\ + // // ep -- b -- c -- d + // assert_channels_list_shape( + // client_c.channel_store(), + // cx_c, + // &[ + // (channel_ep_id, 0), + // (channel_b_id, 1), + // (channel_c_id, 2), + // (channel_d_id, 3), + // (channel_d_id, 2), + // ], + // ); - // Current shape for C: - // /---------\ - // ep -- b -- c -- d - assert_channels_list_shape( - client_c.channel_store(), - cx_c, - &[ - (channel_ep_id, 0), - (channel_b_id, 1), - (channel_c_id, 2), - (channel_d_id, 3), - (channel_d_id, 2), - ], - ); + // client_b + // .channel_store() + // .update(cx_b, |channel_store, cx| { + // channel_store.link_channel(channel_ga_id, channel_b_id, cx) + // }) + // .await + // .unwrap(); - client_b - .channel_store() - .update(cx_b, |channel_store, cx| { - channel_store.link_channel(channel_ga_id, channel_b_id, cx) - }) - .await - .unwrap(); + // // Current shape for B: + // // /---------\ + // // /- ep -- b -- c -- d + // // / \ + // // mu ---------- ga + // assert_channels_list_shape( + // client_b.channel_store(), + // cx_b, + // &[ + // (channel_mu_id, 0), + // (channel_ep_id, 1), + // (channel_b_id, 2), + // (channel_c_id, 3), + // (channel_d_id, 4), + // (channel_d_id, 3), + // (channel_ga_id, 3), + // (channel_ga_id, 1), + // ], + // ); - // Current shape for B: - // /---------\ - // /- ep -- b -- c -- d - // / \ - // mu ---------- ga - assert_channels_list_shape( - client_b.channel_store(), - cx_b, - &[ - (channel_mu_id, 0), - (channel_ep_id, 1), - (channel_b_id, 2), - (channel_c_id, 3), - (channel_d_id, 4), - (channel_d_id, 3), - (channel_ga_id, 3), - (channel_ga_id, 1), - ], - ); + // // Current shape for A: + // // /------\ + // // a - b -- c -- d + // // \-- ga + // assert_channels_list_shape( + // client_a.channel_store(), + // cx_a, + // &[ + // (channel_a_id, 0), + // (channel_b_id, 1), + // (channel_c_id, 2), + // (channel_d_id, 3), + // (channel_d_id, 2), + // (channel_ga_id, 2), + // ], + // ); - // Current shape for A: - // /------\ - // a - b -- c -- d - // \-- ga - assert_channels_list_shape( - client_a.channel_store(), - cx_a, - &[ - (channel_a_id, 0), - (channel_b_id, 1), - (channel_c_id, 2), - (channel_d_id, 3), - (channel_d_id, 2), - (channel_ga_id, 2), - ], - ); - - // Current shape for C: - // /-------\ - // ep -- b -- c -- d - // \-- ga - assert_channels_list_shape( - client_c.channel_store(), - cx_c, - &[ - (channel_ep_id, 0), - (channel_b_id, 1), - (channel_c_id, 2), - (channel_d_id, 3), - (channel_d_id, 2), - (channel_ga_id, 2), - ], - ); + // // Current shape for C: + // // /-------\ + // // ep -- b -- c -- d + // // \-- ga + // assert_channels_list_shape( + // client_c.channel_store(), + // cx_c, + // &[ + // (channel_ep_id, 0), + // (channel_b_id, 1), + // (channel_c_id, 2), + // (channel_d_id, 3), + // (channel_d_id, 2), + // (channel_ga_id, 2), + // ], + // ); } #[derive(Debug, PartialEq)] diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index dcebe35b26..70ea87cfdd 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -120,22 +120,11 @@ struct StartLinkChannelFor { parent_id: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct LinkChannel { - to: ChannelId, -} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct MoveChannel { to: ChannelId, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct UnlinkChannel { - channel_id: ChannelId, - parent_id: ChannelId, -} - type DraggedChannel = (Channel, Option); actions!( @@ -147,8 +136,7 @@ actions!( CollapseSelectedChannel, ExpandSelectedChannel, StartMoveChannel, - StartLinkChannel, - MoveOrLinkToSelected, + MoveSelected, InsertSpace, ] ); @@ -166,11 +154,8 @@ impl_actions!( JoinChannelCall, JoinChannelChat, CopyChannelLink, - LinkChannel, StartMoveChannelFor, - StartLinkChannelFor, MoveChannel, - UnlinkChannel, ToggleSelectedIx ] ); @@ -185,7 +170,7 @@ struct ChannelMoveClipboard { #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum ClipboardIntent { Move, - Link, + // Link, } const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; @@ -238,18 +223,6 @@ pub fn init(cx: &mut AppContext) { }, ); - cx.add_action( - |panel: &mut CollabPanel, - action: &StartLinkChannelFor, - _: &mut ViewContext| { - panel.channel_clipboard = Some(ChannelMoveClipboard { - channel_id: action.channel_id, - parent_id: action.parent_id, - intent: ClipboardIntent::Link, - }) - }, - ); - cx.add_action( |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext| { if let Some((_, path)) = panel.selected_channel() { @@ -263,86 +236,51 @@ pub fn init(cx: &mut AppContext) { ); cx.add_action( - |panel: &mut CollabPanel, _: &StartLinkChannel, _: &mut ViewContext| { - if let Some((_, path)) = panel.selected_channel() { - panel.channel_clipboard = Some(ChannelMoveClipboard { - channel_id: path.channel_id(), - parent_id: path.parent_id(), - intent: ClipboardIntent::Link, - }) - } - }, - ); - - cx.add_action( - |panel: &mut CollabPanel, _: &MoveOrLinkToSelected, cx: &mut ViewContext| { + |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext| { let clipboard = panel.channel_clipboard.take(); if let Some(((selected_channel, _), clipboard)) = panel.selected_channel().zip(clipboard) { match clipboard.intent { - ClipboardIntent::Move if clipboard.parent_id.is_some() => { - let parent_id = clipboard.parent_id.unwrap(); - panel.channel_store.update(cx, |channel_store, cx| { - channel_store - .move_channel( - clipboard.channel_id, - parent_id, - selected_channel.id, - cx, - ) - .detach_and_log_err(cx) - }) - } - _ => panel.channel_store.update(cx, |channel_store, cx| { - channel_store - .link_channel(clipboard.channel_id, selected_channel.id, cx) - .detach_and_log_err(cx) + ClipboardIntent::Move => panel.channel_store.update(cx, |channel_store, cx| { + match clipboard.parent_id { + Some(parent_id) => channel_store.move_channel( + clipboard.channel_id, + parent_id, + selected_channel.id, + cx, + ), + None => channel_store.link_channel( + clipboard.channel_id, + selected_channel.id, + cx, + ), + } + .detach_and_log_err(cx) }), } } }, ); - cx.add_action( - |panel: &mut CollabPanel, action: &LinkChannel, cx: &mut ViewContext| { - if let Some(clipboard) = panel.channel_clipboard.take() { - panel.channel_store.update(cx, |channel_store, cx| { - channel_store - .link_channel(clipboard.channel_id, action.to, cx) - .detach_and_log_err(cx) - }) - } - }, - ); - cx.add_action( |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext| { if let Some(clipboard) = panel.channel_clipboard.take() { panel.channel_store.update(cx, |channel_store, cx| { - if let Some(parent) = clipboard.parent_id { - channel_store - .move_channel(clipboard.channel_id, parent, action.to, cx) - .detach_and_log_err(cx) - } else { - channel_store - .link_channel(clipboard.channel_id, action.to, cx) - .detach_and_log_err(cx) + match clipboard.parent_id { + Some(parent_id) => channel_store.move_channel( + clipboard.channel_id, + parent_id, + action.to, + cx, + ), + None => channel_store.link_channel(clipboard.channel_id, action.to, cx), } + .detach_and_log_err(cx) }) } }, ); - - cx.add_action( - |panel: &mut CollabPanel, action: &UnlinkChannel, cx: &mut ViewContext| { - panel.channel_store.update(cx, |channel_store, cx| { - channel_store - .unlink_channel(action.channel_id, action.parent_id, cx) - .detach_and_log_err(cx) - }) - }, - ); } #[derive(Debug)] @@ -2235,33 +2173,23 @@ impl CollabPanel { this.deploy_channel_context_menu(Some(e.position), &path, ix, cx); } }) - .on_up(MouseButton::Left, move |e, this, cx| { + .on_up(MouseButton::Left, move |_, this, cx| { if let Some((_, dragged_channel)) = cx .global::>() .currently_dragged::(cx.window()) { - if e.modifiers.alt { - this.channel_store.update(cx, |channel_store, cx| { - channel_store - .link_channel(dragged_channel.0.id, channel_id, cx) - .detach_and_log_err(cx) - }) - } else { - this.channel_store.update(cx, |channel_store, cx| { - match dragged_channel.1 { - Some(parent_id) => channel_store.move_channel( - dragged_channel.0.id, - parent_id, - channel_id, - cx, - ), - None => { - channel_store.link_channel(dragged_channel.0.id, channel_id, cx) - } - } - .detach_and_log_err(cx) - }) - } + this.channel_store.update(cx, |channel_store, cx| { + match dragged_channel.1 { + Some(parent_id) => channel_store.move_channel( + dragged_channel.0.id, + parent_id, + channel_id, + cx, + ), + None => channel_store.link_channel(dragged_channel.0.id, channel_id, cx), + } + .detach_and_log_err(cx) + }) } }) .on_move({ @@ -2288,18 +2216,10 @@ impl CollabPanel { }) .as_draggable( (channel.clone(), path.parent_id()), - move |modifiers, (channel, _), cx: &mut ViewContext| { + move |_, (channel, _), cx: &mut ViewContext| { let theme = &theme::current(cx).collab_panel; Flex::::row() - .with_children(modifiers.alt.then(|| { - Svg::new("icons/plus.svg") - .with_color(theme.channel_hash.color) - .constrained() - .with_width(theme.channel_hash.width) - .aligned() - .left() - })) .with_child( Svg::new("icons/hash.svg") .with_color(theme.channel_hash.color) @@ -2743,19 +2663,6 @@ impl CollabPanel { }, ), ContextMenuItem::Separator, - ]); - - if let Some(parent_id) = parent_id { - items.push(ContextMenuItem::action( - "Unlink from parent", - UnlinkChannel { - channel_id: path.channel_id(), - parent_id, - }, - )); - } - - items.extend([ ContextMenuItem::action( "Move this channel", StartMoveChannelFor { @@ -2763,13 +2670,6 @@ impl CollabPanel { parent_id, }, ), - ContextMenuItem::action( - "Link this channel", - StartLinkChannelFor { - channel_id: path.channel_id(), - parent_id, - }, - ), ]); if let Some(channel_name) = channel_name { @@ -2780,12 +2680,6 @@ impl CollabPanel { to: path.channel_id(), }, )); - items.push(ContextMenuItem::action( - format!("Link '#{}' here", channel_name), - LinkChannel { - to: path.channel_id(), - }, - )); } items.extend([ diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 20a47954a4..1bf54dedb7 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -970,10 +970,16 @@ message UpdateChannels { repeated Channel channel_invitations = 5; repeated uint64 remove_channel_invitations = 6; repeated ChannelParticipants channel_participants = 7; + //repeated ChannelRoles channel_roles = 8; repeated UnseenChannelMessage unseen_channel_messages = 9; repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10; } +//message ChannelRoles { +// ChannelRole role = 1; +// uint64 channel_id = 2; +//} + message UnseenChannelMessage { uint64 channel_id = 1; uint64 message_id = 2; From 2b114635678d0176fcba5ef53c28bbf8a44e15b6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 18 Oct 2023 15:43:26 -0600 Subject: [PATCH 05/40] Fix notifications on channel changes --- crates/channel/src/channel_store.rs | 7 + crates/collab/src/db.rs | 17 + crates/collab/src/db/ids.rs | 16 + crates/collab/src/db/queries/channels.rs | 386 +++++++++++++++-------- crates/collab/src/rpc.rs | 229 ++++++-------- crates/collab/src/tests/channel_tests.rs | 205 ++++++++++++ 6 files changed, 598 insertions(+), 262 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index de1f35bef9..4891b5a18e 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -940,6 +940,13 @@ impl ChannelStore { for channel_id in &payload.delete_channels { let channel_id = *channel_id; + if payload + .channels + .iter() + .any(|channel| channel.id == channel_id) + { + continue; + } if let Some(OpenedModelHandle::Open(buffer)) = self.opened_buffers.remove(&channel_id) { diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 32cf5cb20a..4d73d27a47 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -436,6 +436,23 @@ pub struct Channel { pub role: ChannelRole, } +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ChannelMember { + pub role: ChannelRole, + pub user_id: UserId, + pub kind: proto::channel_member::Kind, +} + +impl ChannelMember { + pub fn to_proto(&self) -> proto::ChannelMember { + proto::ChannelMember { + role: self.role.into(), + user_id: self.user_id.to_proto(), + kind: self.kind.into(), + } + } +} + #[derive(Debug, PartialEq)] pub struct ChannelsForUser { pub channels: ChannelGraph, diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 55aae7ed3b..4c6e7116ee 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -114,6 +114,22 @@ impl ChannelRole { other } } + + pub fn can_see_all_descendants(&self) -> bool { + use ChannelRole::*; + match self { + Admin | Member => true, + Guest | Banned => false, + } + } + + pub fn can_only_see_public_descendants(&self) -> bool { + use ChannelRole::*; + match self { + Guest => true, + Admin | Member | Banned => false, + } + } } impl From for ChannelRole { diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 575f55fe02..36d162d0ae 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -633,32 +633,84 @@ impl Database { .await } - pub async fn get_channel_members_and_roles( + pub async fn participants_to_notify_for_channel_change( &self, - id: ChannelId, - ) -> Result> { + new_parent: ChannelId, + admin_id: UserId, + ) -> Result> { self.transaction(|tx| async move { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryUserIdsAndRoles { - UserId, - Role, + let mut results: Vec<(UserId, ChannelsForUser)> = Vec::new(); + + let members = self + .get_channel_participant_details_internal(new_parent, admin_id, &*tx) + .await?; + + dbg!(&members); + + for member in members.iter() { + if !member.role.can_see_all_descendants() { + continue; + } + results.push(( + member.user_id, + self.get_user_channels( + member.user_id, + vec![channel_member::Model { + id: Default::default(), + channel_id: new_parent, + user_id: member.user_id, + role: member.role, + accepted: true, + }], + &*tx, + ) + .await?, + )) } - let ancestor_ids = self.get_channel_ancestors(id, &*tx).await?; - let user_ids_and_roles = channel_member::Entity::find() - .distinct() - .filter( - channel_member::Column::ChannelId - .is_in(ancestor_ids.iter().copied()) - .and(channel_member::Column::Accepted.eq(true)), - ) - .select_only() - .column(channel_member::Column::UserId) - .column(channel_member::Column::Role) - .into_values::<_, QueryUserIdsAndRoles>() - .all(&*tx) - .await?; - Ok(user_ids_and_roles) + let public_parent = self + .public_path_to_channel_internal(new_parent, &*tx) + .await? + .last() + .copied(); + + let Some(public_parent) = public_parent else { + return Ok(results); + }; + + // could save some time in the common case by skipping this if the + // new channel is not public and has no public descendants. + let public_members = if public_parent == new_parent { + members + } else { + self.get_channel_participant_details_internal(public_parent, admin_id, &*tx) + .await? + }; + + dbg!(&public_members); + + for member in public_members { + if !member.role.can_only_see_public_descendants() { + continue; + }; + results.push(( + member.user_id, + self.get_user_channels( + member.user_id, + vec![channel_member::Model { + id: Default::default(), + channel_id: public_parent, + user_id: member.user_id, + role: member.role, + accepted: true, + }], + &*tx, + ) + .await?, + )) + } + + Ok(results) }) .await } @@ -696,103 +748,119 @@ impl Database { .await } + pub async fn get_channel_participant_details_internal( + &self, + channel_id: ChannelId, + admin_id: UserId, + tx: &DatabaseTransaction, + ) -> Result> { + self.check_user_is_channel_admin(channel_id, admin_id, &*tx) + .await?; + + let channel_visibility = channel::Entity::find() + .filter(channel::Column::Id.eq(channel_id)) + .one(&*tx) + .await? + .map(|channel| channel.visibility) + .unwrap_or(ChannelVisibility::Members); + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryMemberDetails { + UserId, + Role, + IsDirectMember, + Accepted, + Visibility, + } + + let tx = tx; + let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?; + let mut stream = channel_member::Entity::find() + .left_join(channel::Entity) + .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) + .select_only() + .column(channel_member::Column::UserId) + .column(channel_member::Column::Role) + .column_as( + channel_member::Column::ChannelId.eq(channel_id), + QueryMemberDetails::IsDirectMember, + ) + .column(channel_member::Column::Accepted) + .column(channel::Column::Visibility) + .into_values::<_, QueryMemberDetails>() + .stream(&*tx) + .await?; + + let mut user_details: HashMap = HashMap::default(); + + while let Some(user_membership) = stream.next().await { + let (user_id, channel_role, is_direct_member, is_invite_accepted, visibility): ( + UserId, + ChannelRole, + bool, + bool, + ChannelVisibility, + ) = user_membership?; + let kind = match (is_direct_member, is_invite_accepted) { + (true, true) => proto::channel_member::Kind::Member, + (true, false) => proto::channel_member::Kind::Invitee, + (false, true) => proto::channel_member::Kind::AncestorMember, + (false, false) => continue, + }; + + if channel_role == ChannelRole::Guest + && visibility != ChannelVisibility::Public + && channel_visibility != ChannelVisibility::Public + { + continue; + } + + if let Some(details_mut) = user_details.get_mut(&user_id) { + if channel_role.should_override(details_mut.role) { + details_mut.role = channel_role; + } + if kind == Kind::Member { + details_mut.kind = kind; + // the UI is going to be a bit confusing if you already have permissions + // that are greater than or equal to the ones you're being invited to. + } else if kind == Kind::Invitee && details_mut.kind == Kind::AncestorMember { + details_mut.kind = kind; + } + } else { + user_details.insert( + user_id, + ChannelMember { + user_id, + kind, + role: channel_role, + }, + ); + } + } + + Ok(user_details + .into_iter() + .map(|(_, details)| details) + .collect()) + } + pub async fn get_channel_participant_details( &self, channel_id: ChannelId, admin_id: UserId, ) -> Result> { - self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, admin_id, &*tx) - .await?; + let members = self + .transaction(move |tx| async move { + Ok(self + .get_channel_participant_details_internal(channel_id, admin_id, &*tx) + .await?) + }) + .await?; - let channel_visibility = channel::Entity::find() - .filter(channel::Column::Id.eq(channel_id)) - .one(&*tx) - .await? - .map(|channel| channel.visibility) - .unwrap_or(ChannelVisibility::Members); - - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryMemberDetails { - UserId, - Role, - IsDirectMember, - Accepted, - Visibility, - } - - let tx = tx; - let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?; - let mut stream = channel_member::Entity::find() - .left_join(channel::Entity) - .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) - .select_only() - .column(channel_member::Column::UserId) - .column(channel_member::Column::Role) - .column_as( - channel_member::Column::ChannelId.eq(channel_id), - QueryMemberDetails::IsDirectMember, - ) - .column(channel_member::Column::Accepted) - .column(channel::Column::Visibility) - .into_values::<_, QueryMemberDetails>() - .stream(&*tx) - .await?; - - struct UserDetail { - kind: Kind, - channel_role: ChannelRole, - } - let mut user_details: HashMap = HashMap::default(); - - while let Some(user_membership) = stream.next().await { - let (user_id, channel_role, is_direct_member, is_invite_accepted, visibility): ( - UserId, - ChannelRole, - bool, - bool, - ChannelVisibility, - ) = user_membership?; - let kind = match (is_direct_member, is_invite_accepted) { - (true, true) => proto::channel_member::Kind::Member, - (true, false) => proto::channel_member::Kind::Invitee, - (false, true) => proto::channel_member::Kind::AncestorMember, - (false, false) => continue, - }; - - if channel_role == ChannelRole::Guest - && visibility != ChannelVisibility::Public - && channel_visibility != ChannelVisibility::Public - { - continue; - } - - if let Some(details_mut) = user_details.get_mut(&user_id) { - if channel_role.should_override(details_mut.channel_role) { - details_mut.channel_role = channel_role; - } - if kind == Kind::Member { - details_mut.kind = kind; - // the UI is going to be a bit confusing if you already have permissions - // that are greater than or equal to the ones you're being invited to. - } else if kind == Kind::Invitee && details_mut.kind == Kind::AncestorMember { - details_mut.kind = kind; - } - } else { - user_details.insert(user_id, UserDetail { kind, channel_role }); - } - } - - Ok(user_details - .into_iter() - .map(|(user_id, details)| proto::ChannelMember { - user_id: user_id.to_proto(), - kind: details.kind.into(), - role: details.channel_role.into(), - }) - .collect()) - }) - .await + Ok(members + .into_iter() + .map(|channel_member| channel_member.to_proto()) + .collect()) } pub async fn get_channel_participants_internal( @@ -883,6 +951,60 @@ impl Database { Ok(row) } + // ordered from higher in tree to lower + // only considers one path to a channel + // includes the channel itself + pub async fn path_to_channel(&self, channel_id: ChannelId) -> Result> { + self.transaction(move |tx| async move { + Ok(self.path_to_channel_internal(channel_id, &*tx).await?) + }) + .await + } + + pub async fn parent_channel_id(&self, channel_id: ChannelId) -> Result> { + let path = self.path_to_channel(channel_id).await?; + if path.len() >= 2 { + Ok(Some(path[path.len() - 2])) + } else { + Ok(None) + } + } + + pub async fn public_parent_channel_id( + &self, + channel_id: ChannelId, + ) -> Result> { + let path = self.path_to_channel(channel_id).await?; + if path.len() >= 2 && path.last().copied() == Some(channel_id) { + Ok(Some(path[path.len() - 2])) + } else { + Ok(path.last().copied()) + } + } + + pub async fn path_to_channel_internal( + &self, + channel_id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result> { + let arbitary_path = channel_path::Entity::find() + .filter(channel_path::Column::ChannelId.eq(channel_id)) + .order_by(channel_path::Column::IdPath, sea_orm::Order::Desc) + .one(tx) + .await?; + + let Some(path) = arbitary_path else { + return Ok(vec![]); + }; + + Ok(path + .id_path + .trim_matches('/') + .split('/') + .map(|id| ChannelId::from_proto(id.parse().unwrap())) + .collect()) + } + // ordered from higher in tree to lower // only considers one path to a channel // includes the channel itself @@ -903,22 +1025,7 @@ impl Database { channel_id: ChannelId, tx: &DatabaseTransaction, ) -> Result> { - let arbitary_path = channel_path::Entity::find() - .filter(channel_path::Column::ChannelId.eq(channel_id)) - .order_by(channel_path::Column::IdPath, sea_orm::Order::Desc) - .one(tx) - .await?; - - let Some(path) = arbitary_path else { - return Ok(vec![]); - }; - - let ancestor_ids: Vec = path - .id_path - .trim_matches('/') - .split('/') - .map(|id| ChannelId::from_proto(id.parse().unwrap())) - .collect(); + let ancestor_ids = self.path_to_channel_internal(channel_id, &*tx).await?; let rows = channel::Entity::find() .filter(channel::Column::Id.is_in(ancestor_ids.iter().copied())) @@ -1044,6 +1151,27 @@ impl Database { Ok(channel_ids) } + // returns all ids of channels in the tree under this channel_id. + pub async fn get_channel_descendant_ids( + &self, + channel_id: ChannelId, + ) -> Result> { + self.transaction(|tx| async move { + let pairs = self.get_channel_descendants([channel_id], &*tx).await?; + + let mut results: HashSet = HashSet::default(); + for ChannelEdge { + parent_id: _, + channel_id, + } in pairs + { + results.insert(ChannelId::from_proto(channel_id)); + } + Ok(results) + }) + .await + } + // Returns the channel desendants as a sorted list of edges for further processing. // The edges are sorted such that you will see unknown channel ids as children // before you see them as parents. diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index c7a2ba88b1..18fbc0d7bc 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2220,26 +2220,13 @@ async fn create_channel( return Ok(()); }; - let members: Vec = db - .get_channel_participant_details(parent_id, session.user_id) - .await? - .into_iter() - .filter(|member| { - member.role() == proto::ChannelRole::Admin - || member.role() == proto::ChannelRole::Member - }) - .collect(); - - let mut updates: HashMap = HashMap::default(); - - for member in members { - let user_id = UserId::from_proto(member.user_id); - let channels = db.get_channel_for_user(parent_id, user_id).await?; - updates.insert(user_id, build_initial_channels_update(channels, vec![])); - } + let updates = db + .participants_to_notify_for_channel_change(parent_id, session.user_id) + .await?; let connection_pool = session.connection_pool().await; - for (user_id, update) in updates { + for (user_id, channels) in updates { + let update = build_initial_channels_update(channels, vec![]); for connection_id in connection_pool.user_connection_ids(user_id) { if user_id == session.user_id { continue; @@ -2353,31 +2340,55 @@ async fn set_channel_visibility( let channel_id = ChannelId::from_proto(request.channel_id); let visibility = request.visibility().into(); - let channel = db - .set_channel_visibility(channel_id, visibility, session.user_id) - .await?; - - let members = db + let previous_members = db .get_channel_participant_details(channel_id, session.user_id) .await?; + db.set_channel_visibility(channel_id, visibility, session.user_id) + .await?; + + let mut updates: HashMap = db + .participants_to_notify_for_channel_change(channel_id, session.user_id) + .await? + .into_iter() + .collect(); + + let mut participants_who_lost_access: HashSet = HashSet::default(); + match visibility { + ChannelVisibility::Members => { + for member in previous_members { + if ChannelRole::from(member.role()).can_only_see_public_descendants() { + participants_who_lost_access.insert(UserId::from_proto(member.user_id)); + } + } + } + ChannelVisibility::Public => { + if let Some(public_parent_id) = db.public_parent_channel_id(channel_id).await? { + let parent_updates = db + .participants_to_notify_for_channel_change(public_parent_id, session.user_id) + .await?; + + for (user_id, channels) in parent_updates { + updates.insert(user_id, channels); + } + } + } + } + let connection_pool = session.connection_pool().await; - // TODO: notify people who were guests and are now not allowed. - for member in members { - for connection_id in connection_pool.user_connection_ids(UserId::from_proto(member.user_id)) - { - session.peer.send( - connection_id, - proto::UpdateChannels { - channels: vec![proto::Channel { - id: channel.id.to_proto(), - name: channel.name.clone(), - visibility: channel.visibility.into(), - role: member.role.into(), - }], - ..Default::default() - }, - )?; + for (user_id, channels) in updates { + let update = build_initial_channels_update(channels, vec![]); + for connection_id in connection_pool.user_connection_ids(user_id) { + session.peer.send(connection_id, update.clone())?; + } + } + for user_id in participants_who_lost_access { + let update = proto::UpdateChannels { + delete_channels: vec![channel_id.to_proto()], + ..Default::default() + }; + for connection_id in connection_pool.user_connection_ids(user_id) { + session.peer.send(connection_id, update.clone())?; } } @@ -2485,42 +2496,20 @@ async fn link_channel( // TODO: Remove this restriction once we have symlinks db.assert_root_channel(channel_id).await?; - let channels_to_send = db.link_channel(session.user_id, channel_id, to).await?; - let members = db.get_channel_members_and_roles(to).await?; + db.link_channel(session.user_id, channel_id, to).await?; + + let member_updates = db + .participants_to_notify_for_channel_change(to, session.user_id) + .await?; + + dbg!(&member_updates); + let connection_pool = session.connection_pool().await; - for (member_id, role) in members { - let build_channel_proto = |channel: &db::Channel| proto::Channel { - id: channel.id.to_proto(), - visibility: channel.visibility.into(), - name: channel.name.clone(), - role: role.into(), - }; - + for (member_id, channels) in member_updates { + let update = build_initial_channels_update(channels, vec![]); for connection_id in connection_pool.user_connection_ids(member_id) { - let channels: Vec<_> = if role == ChannelRole::Guest { - channels_to_send - .channels - .iter() - .filter(|channel| channel.visibility != ChannelVisibility::Public) - .map(build_channel_proto) - .collect() - } else { - channels_to_send - .channels - .iter() - .map(build_channel_proto) - .collect() - }; - - session.peer.send( - connection_id, - proto::UpdateChannels { - channels, - insert_edge: channels_to_send.edges.clone(), - ..Default::default() - }, - )?; + session.peer.send(connection_id, update.clone())?; } } @@ -2548,11 +2537,11 @@ async fn move_channel( let from_parent = ChannelId::from_proto(request.from); let to = ChannelId::from_proto(request.to); - let from_public_parent = db - .public_path_to_channel(from_parent) - .await? - .last() - .copied(); + let previous_participants = db + .get_channel_participant_details(channel_id, session.user_id) + .await?; + + debug_assert_eq!(db.parent_channel_id(channel_id).await?, Some(from_parent)); let channels_to_send = db .move_channel(session.user_id, channel_id, from_parent, to) @@ -2563,68 +2552,42 @@ async fn move_channel( return Ok(()); } - let to_public_parent = db.public_path_to_channel(to).await?.last().cloned(); + let updates = db + .participants_to_notify_for_channel_change(to, session.user_id) + .await?; - let members_from = db - .get_channel_participant_details(from_parent, session.user_id) - .await? - .into_iter() - .filter(|member| { - member.role() == proto::ChannelRole::Admin || member.role() == proto::ChannelRole::Guest - }); + let mut participants_who_lost_access: HashSet = HashSet::default(); + let mut channels_to_delete = db.get_channel_descendant_ids(channel_id).await?; + channels_to_delete.insert(channel_id); - let members_to = db - .get_channel_participant_details(to, session.user_id) - .await? - .into_iter() - .filter(|member| { - member.role() == proto::ChannelRole::Admin || member.role() == proto::ChannelRole::Guest - }); - - let mut updates: HashMap = HashMap::default(); - - for member in members_to { - let user_id = UserId::from_proto(member.user_id); - let channels = db.get_channel_for_user(to, user_id).await?; - updates.insert(user_id, build_initial_channels_update(channels, vec![])); - } - - if let Some(to_public_parent) = to_public_parent { - // only notify guests of public channels (admins/members are notified by members_to above, and banned users don't care) - let public_members_to = db - .get_channel_participant_details(to, session.user_id) - .await? - .into_iter() - .filter(|member| member.role() == proto::ChannelRole::Guest); - - for member in public_members_to { - let user_id = UserId::from_proto(member.user_id); - if updates.contains_key(&user_id) { - continue; - } - let channels = db.get_channel_for_user(to_public_parent, user_id).await?; - updates.insert(user_id, build_initial_channels_update(channels, vec![])); + for previous_participant in previous_participants.iter() { + let user_id = UserId::from_proto(previous_participant.user_id); + if previous_participant.kind() == proto::channel_member::Kind::AncestorMember { + participants_who_lost_access.insert(user_id); } } - for member in members_from { - let user_id = UserId::from_proto(member.user_id); - let update = updates - .entry(user_id) - .or_insert(proto::UpdateChannels::default()); - update.delete_edge.push(proto::ChannelEdge { - channel_id: channel_id.to_proto(), - parent_id: from_parent.to_proto(), - }) - } - - if let Some(_from_public_parent) = from_public_parent { - // TODO: for each guest member of the old public parent - // delete the edge that they could see (from the from_public_parent down) - } - let connection_pool = session.connection_pool().await; - for (user_id, update) in updates { + for (user_id, channels) in updates { + let mut update = build_initial_channels_update(channels, vec![]); + update.delete_channels = channels_to_delete + .iter() + .map(|channel_id| channel_id.to_proto()) + .collect(); + participants_who_lost_access.remove(&user_id); + for connection_id in connection_pool.user_connection_ids(user_id) { + session.peer.send(connection_id, update.clone())?; + } + } + + for user_id in participants_who_lost_access { + let update = proto::UpdateChannels { + delete_channels: channels_to_delete + .iter() + .map(|channel_id| channel_id.to_proto()) + .collect(), + ..Default::default() + }; for connection_id in connection_pool.user_connection_ids(user_id) { session.peer.send(connection_id, update.clone())?; } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index f982f05ee3..b9425cc629 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -5,6 +5,7 @@ use crate::{ use call::ActiveCall; use channel::{ChannelId, ChannelMembership, ChannelStore}; use client::User; +use futures::future::try_join_all; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{ proto::{self, ChannelRole}, @@ -913,6 +914,210 @@ async fn test_lost_channel_creation( ], ); } + +#[gpui::test] +async fn test_channel_link_notifications( + deterministic: Arc, + 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 user_b = client_b.user_id().unwrap(); + let user_c = client_c.user_id().unwrap(); + + let channels = server + .make_channel_tree(&[("zed", None)], (&client_a, cx_a)) + .await; + let zed_channel = channels[0]; + + try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| { + [ + channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx), + channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Member, cx), + channel_store.invite_member(zed_channel, user_c, proto::ChannelRole::Guest, cx), + ] + })) + .await + .unwrap(); + + deterministic.run_until_parked(); + + client_b + .channel_store() + .update(cx_b, |channel_store, _| { + channel_store.respond_to_channel_invite(zed_channel, true) + }) + .await + .unwrap(); + + client_c + .channel_store() + .update(cx_c, |channel_store, _| { + channel_store.respond_to_channel_invite(zed_channel, true) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + // we have an admin (a), member (b) and guest (c) all part of the zed channel. + + // create a new private sub-channel + // create a new priate channel, make it public, and move it under the previous one, and verify it shows for b and c + let active_channel = client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.create_channel("active", Some(zed_channel), cx) + }) + .await + .unwrap(); + + // the new channel shows for b and not c + assert_channels_list_shape( + client_a.channel_store(), + cx_a, + &[(zed_channel, 0), (active_channel, 1)], + ); + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[(zed_channel, 0), (active_channel, 1)], + ); + assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]); + + let vim_channel = client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.create_channel("vim", None, cx) + }) + .await + .unwrap(); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx) + }) + .await + .unwrap(); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.link_channel(vim_channel, active_channel, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + // the new channel shows for b and c + assert_channels_list_shape( + client_a.channel_store(), + cx_a, + &[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)], + ); + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)], + ); + assert_channels_list_shape( + client_c.channel_store(), + cx_c, + &[(zed_channel, 0), (vim_channel, 1)], + ); + + let helix_channel = client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.create_channel("helix", None, cx) + }) + .await + .unwrap(); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.link_channel(helix_channel, vim_channel, cx) + }) + .await + .unwrap(); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_channel_visibility( + helix_channel, + proto::ChannelVisibility::Public, + cx, + ) + }) + .await + .unwrap(); + + // the new channel shows for b and c + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + (zed_channel, 0), + (active_channel, 1), + (vim_channel, 2), + (helix_channel, 3), + ], + ); + assert_channels_list_shape( + client_c.channel_store(), + cx_c, + &[(zed_channel, 0), (vim_channel, 1), (helix_channel, 2)], + ); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Members, cx) + }) + .await + .unwrap(); + + // the members-only channel is still shown for c, but hidden for b + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + (zed_channel, 0), + (active_channel, 1), + (vim_channel, 2), + (helix_channel, 3), + ], + ); + client_b + .channel_store() + .read_with(cx_b, |channel_store, _| { + assert_eq!( + channel_store + .channel_for_id(vim_channel) + .unwrap() + .visibility, + proto::ChannelVisibility::Members + ) + }); + + assert_channels_list_shape( + client_c.channel_store(), + cx_c, + &[(zed_channel, 0), (helix_channel, 1)], + ); +} + #[gpui::test] async fn test_guest_access( deterministic: Arc, From 3853009d920b95defec88e87fa8760b5bcf4f875 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 18 Oct 2023 19:27:00 -0600 Subject: [PATCH 06/40] Refactor to avoid some (mostly hypothetical) races Tidy up added code to reduce duplicity of X and X_internals. --- crates/channel/src/channel_store_tests.rs | 44 +- crates/collab/src/db.rs | 36 + crates/collab/src/db/queries/buffers.rs | 4 +- crates/collab/src/db/queries/channels.rs | 464 +++++++------ crates/collab/src/db/queries/messages.rs | 4 +- crates/collab/src/db/queries/rooms.rs | 14 +- crates/collab/src/db/tests/channel_tests.rs | 643 +++++++++--------- crates/collab/src/db/tests/message_tests.rs | 8 +- crates/collab/src/rpc.rs | 222 ++---- .../src/tests/random_channel_buffer_tests.rs | 2 +- crates/collab/src/tests/test_server.rs | 32 - crates/collab_ui/src/collab_panel.rs | 1 - crates/rpc/proto/zed.proto | 6 - 13 files changed, 715 insertions(+), 765 deletions(-) diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 69c0cd37fc..1358378d16 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -36,8 +36,8 @@ fn test_update_channels(cx: &mut AppContext) { &channel_store, &[ // - (0, "a".to_string(), false), - (0, "b".to_string(), true), + (0, "a".to_string(), proto::ChannelRole::Member), + (0, "b".to_string(), proto::ChannelRole::Admin), ], cx, ); @@ -50,7 +50,7 @@ fn test_update_channels(cx: &mut AppContext) { id: 3, name: "x".to_string(), visibility: proto::ChannelVisibility::Members as i32, - role: proto::ChannelRole::Member.into(), + role: proto::ChannelRole::Admin.into(), }, proto::Channel { id: 4, @@ -76,10 +76,10 @@ fn test_update_channels(cx: &mut AppContext) { assert_channels( &channel_store, &[ - (0, "a".to_string(), false), - (1, "y".to_string(), false), - (0, "b".to_string(), true), - (1, "x".to_string(), true), + (0, "a".to_string(), proto::ChannelRole::Member), + (1, "y".to_string(), proto::ChannelRole::Member), + (0, "b".to_string(), proto::ChannelRole::Admin), + (1, "x".to_string(), proto::ChannelRole::Admin), ], cx, ); @@ -131,9 +131,9 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { &channel_store, &[ // - (0, "a".to_string(), true), - (1, "b".to_string(), true), - (2, "c".to_string(), true), + (0, "a".to_string(), proto::ChannelRole::Admin), + (1, "b".to_string(), proto::ChannelRole::Admin), + (2, "c".to_string(), proto::ChannelRole::Admin), ], cx, ); @@ -148,7 +148,11 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { ); // Make sure that the 1/2/3 path is gone - assert_channels(&channel_store, &[(0, "a".to_string(), true)], cx); + assert_channels( + &channel_store, + &[(0, "a".to_string(), proto::ChannelRole::Admin)], + cx, + ); } #[gpui::test] @@ -165,13 +169,17 @@ async fn test_channel_messages(cx: &mut TestAppContext) { id: channel_id, name: "the-channel".to_string(), visibility: proto::ChannelVisibility::Members as i32, - role: proto::ChannelRole::Admin.into(), + role: proto::ChannelRole::Member.into(), }], ..Default::default() }); cx.foreground().run_until_parked(); cx.read(|cx| { - assert_channels(&channel_store, &[(0, "the-channel".to_string(), false)], cx); + assert_channels( + &channel_store, + &[(0, "the-channel".to_string(), proto::ChannelRole::Member)], + cx, + ); }); let get_users = server.receive::().await.unwrap(); @@ -366,19 +374,13 @@ fn update_channels( #[track_caller] fn assert_channels( channel_store: &ModelHandle, - expected_channels: &[(usize, String, bool)], + expected_channels: &[(usize, String, proto::ChannelRole)], cx: &AppContext, ) { let actual = channel_store.read_with(cx, |store, _| { store .channel_dag_entries() - .map(|(depth, channel)| { - ( - depth, - channel.name.to_string(), - store.is_channel_admin(channel.id), - ) - }) + .map(|(depth, channel)| (depth, channel.name.to_string(), channel.role)) .collect::>() }); assert_eq!(actual, expected_channels); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 4d73d27a47..4f49b7ca39 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -428,6 +428,31 @@ pub struct NewUserResult { pub signup_device_id: Option, } +#[derive(Debug)] +pub struct MoveChannelResult { + pub participants_to_update: HashMap, + pub participants_to_remove: HashSet, + pub moved_channels: HashSet, +} + +#[derive(Debug)] +pub struct RenameChannelResult { + pub channel: Channel, + pub participants_to_update: HashMap, +} + +#[derive(Debug)] +pub struct CreateChannelResult { + pub channel: Channel, + pub participants_to_update: Vec<(UserId, ChannelsForUser)>, +} + +#[derive(Debug)] +pub struct SetChannelVisibilityResult { + pub participants_to_update: HashMap, + pub participants_to_remove: HashSet, +} + #[derive(FromQueryResult, Debug, PartialEq, Eq, Hash)] pub struct Channel { pub id: ChannelId, @@ -436,6 +461,17 @@ pub struct Channel { pub role: ChannelRole, } +impl Channel { + pub fn to_proto(&self) -> proto::Channel { + proto::Channel { + id: self.id.to_proto(), + name: self.name.clone(), + visibility: self.visibility.into(), + role: self.role.into(), + } + } +} + #[derive(Debug, PartialEq, Eq, Hash)] pub struct ChannelMember { pub role: ChannelRole, diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 1b8467c75a..3aa9cff171 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -482,9 +482,7 @@ impl Database { ) .await?; - channel_members = self - .get_channel_participants_internal(channel_id, &*tx) - .await?; + channel_members = self.get_channel_participants(channel_id, &*tx).await?; let collaborators = self .get_channel_buffer_collaborators_internal(channel_id, &*tx) .await?; diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 36d162d0ae..f7b7f6085f 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -16,20 +16,39 @@ impl Database { .await } + #[cfg(test)] pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result { - self.create_channel(name, None, creator_id).await + Ok(self + .create_channel(name, None, creator_id) + .await? + .channel + .id) + } + + #[cfg(test)] + pub async fn create_sub_channel( + &self, + name: &str, + parent: ChannelId, + creator_id: UserId, + ) -> Result { + Ok(self + .create_channel(name, Some(parent), creator_id) + .await? + .channel + .id) } pub async fn create_channel( &self, name: &str, parent: Option, - creator_id: UserId, - ) -> Result { + admin_id: UserId, + ) -> Result { let name = Self::sanitize_channel_name(name)?; self.transaction(move |tx| async move { if let Some(parent) = parent { - self.check_user_is_channel_admin(parent, creator_id, &*tx) + self.check_user_is_channel_admin(parent, admin_id, &*tx) .await?; } @@ -71,17 +90,34 @@ impl Database { .await?; } - channel_member::ActiveModel { - id: ActiveValue::NotSet, - channel_id: ActiveValue::Set(channel.id), - user_id: ActiveValue::Set(creator_id), - accepted: ActiveValue::Set(true), - role: ActiveValue::Set(ChannelRole::Admin), + if parent.is_none() { + channel_member::ActiveModel { + id: ActiveValue::NotSet, + channel_id: ActiveValue::Set(channel.id), + user_id: ActiveValue::Set(admin_id), + accepted: ActiveValue::Set(true), + role: ActiveValue::Set(ChannelRole::Admin), + } + .insert(&*tx) + .await?; } - .insert(&*tx) - .await?; - Ok(channel.id) + let participants_to_update = if let Some(parent) = parent { + self.participants_to_notify_for_channel_change(parent, &*tx) + .await? + } else { + vec![] + }; + + Ok(CreateChannelResult { + channel: Channel { + id: channel.id, + visibility: channel.visibility, + name: channel.name, + role: ChannelRole::Admin, + }, + participants_to_update, + }) }) .await } @@ -132,7 +168,7 @@ impl Database { && channel.as_ref().map(|c| c.visibility) == Some(ChannelVisibility::Public) { let channel_id_to_join = self - .public_path_to_channel_internal(channel_id, &*tx) + .public_path_to_channel(channel_id, &*tx) .await? .first() .cloned() @@ -178,13 +214,17 @@ impl Database { &self, channel_id: ChannelId, visibility: ChannelVisibility, - user_id: UserId, - ) -> Result { + admin_id: UserId, + ) -> Result { self.transaction(move |tx| async move { - self.check_user_is_channel_admin(channel_id, user_id, &*tx) + self.check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; - let channel = channel::ActiveModel { + let previous_members = self + .get_channel_participant_details_internal(channel_id, &*tx) + .await?; + + channel::ActiveModel { id: ActiveValue::Unchanged(channel_id), visibility: ActiveValue::Set(visibility), ..Default::default() @@ -192,7 +232,40 @@ impl Database { .update(&*tx) .await?; - Ok(channel) + let mut participants_to_update: HashMap = self + .participants_to_notify_for_channel_change(channel_id, &*tx) + .await? + .into_iter() + .collect(); + + let mut participants_to_remove: HashSet = HashSet::default(); + match visibility { + ChannelVisibility::Members => { + for member in previous_members { + if member.role.can_only_see_public_descendants() { + participants_to_remove.insert(member.user_id); + } + } + } + ChannelVisibility::Public => { + if let Some(public_parent_id) = + self.public_parent_channel_id(channel_id, &*tx).await? + { + let parent_updates = self + .participants_to_notify_for_channel_change(public_parent_id, &*tx) + .await?; + + for (user_id, channels) in parent_updates { + participants_to_update.insert(user_id, channels); + } + } + } + } + + Ok(SetChannelVisibilityResult { + participants_to_update, + participants_to_remove, + }) }) .await } @@ -303,14 +376,14 @@ impl Database { pub async fn rename_channel( &self, channel_id: ChannelId, - user_id: UserId, + admin_id: UserId, new_name: &str, - ) -> Result { + ) -> Result { self.transaction(move |tx| async move { let new_name = Self::sanitize_channel_name(new_name)?.to_string(); let role = self - .check_user_is_channel_admin(channel_id, user_id, &*tx) + .check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; let channel = channel::ActiveModel { @@ -321,11 +394,31 @@ impl Database { .update(&*tx) .await?; - Ok(Channel { - id: channel.id, - name: channel.name, - visibility: channel.visibility, - role, + let participants = self + .get_channel_participant_details_internal(channel_id, &*tx) + .await?; + + Ok(RenameChannelResult { + channel: Channel { + id: channel.id, + name: channel.name, + visibility: channel.visibility, + role, + }, + participants_to_update: participants + .iter() + .map(|participant| { + ( + participant.user_id, + Channel { + id: channel.id, + name: new_name.clone(), + visibility: channel.visibility, + role: participant.role, + }, + ) + }) + .collect(), }) }) .await @@ -628,91 +721,83 @@ impl Database { }) } - pub async fn get_channel_members(&self, id: ChannelId) -> Result> { - self.transaction(|tx| async move { self.get_channel_participants_internal(id, &*tx).await }) - .await - } - - pub async fn participants_to_notify_for_channel_change( + async fn participants_to_notify_for_channel_change( &self, new_parent: ChannelId, - admin_id: UserId, + tx: &DatabaseTransaction, ) -> Result> { - self.transaction(|tx| async move { - let mut results: Vec<(UserId, ChannelsForUser)> = Vec::new(); + let mut results: Vec<(UserId, ChannelsForUser)> = Vec::new(); - let members = self - .get_channel_participant_details_internal(new_parent, admin_id, &*tx) - .await?; + let members = self + .get_channel_participant_details_internal(new_parent, &*tx) + .await?; - dbg!(&members); + dbg!(&members); - for member in members.iter() { - if !member.role.can_see_all_descendants() { - continue; - } - results.push(( - member.user_id, - self.get_user_channels( - member.user_id, - vec![channel_member::Model { - id: Default::default(), - channel_id: new_parent, - user_id: member.user_id, - role: member.role, - accepted: true, - }], - &*tx, - ) - .await?, - )) + for member in members.iter() { + if !member.role.can_see_all_descendants() { + continue; } + results.push(( + member.user_id, + self.get_user_channels( + member.user_id, + vec![channel_member::Model { + id: Default::default(), + channel_id: new_parent, + user_id: member.user_id, + role: member.role, + accepted: true, + }], + &*tx, + ) + .await?, + )) + } - let public_parent = self - .public_path_to_channel_internal(new_parent, &*tx) + let public_parent = self + .public_path_to_channel(new_parent, &*tx) + .await? + .last() + .copied(); + + let Some(public_parent) = public_parent else { + return Ok(results); + }; + + // could save some time in the common case by skipping this if the + // new channel is not public and has no public descendants. + let public_members = if public_parent == new_parent { + members + } else { + self.get_channel_participant_details_internal(public_parent, &*tx) .await? - .last() - .copied(); + }; - let Some(public_parent) = public_parent else { - return Ok(results); + dbg!(&public_members); + + for member in public_members { + if !member.role.can_only_see_public_descendants() { + continue; }; - - // could save some time in the common case by skipping this if the - // new channel is not public and has no public descendants. - let public_members = if public_parent == new_parent { - members - } else { - self.get_channel_participant_details_internal(public_parent, admin_id, &*tx) - .await? - }; - - dbg!(&public_members); - - for member in public_members { - if !member.role.can_only_see_public_descendants() { - continue; - }; - results.push(( + results.push(( + member.user_id, + self.get_user_channels( member.user_id, - self.get_user_channels( - member.user_id, - vec![channel_member::Model { - id: Default::default(), - channel_id: public_parent, - user_id: member.user_id, - role: member.role, - accepted: true, - }], - &*tx, - ) - .await?, - )) - } + vec![channel_member::Model { + id: Default::default(), + channel_id: public_parent, + user_id: member.user_id, + role: member.role, + accepted: true, + }], + &*tx, + ) + .await?, + )) + } - Ok(results) - }) - .await + Ok(results) } pub async fn set_channel_member_role( @@ -748,15 +833,11 @@ impl Database { .await } - pub async fn get_channel_participant_details_internal( + async fn get_channel_participant_details_internal( &self, channel_id: ChannelId, - admin_id: UserId, tx: &DatabaseTransaction, ) -> Result> { - self.check_user_is_channel_admin(channel_id, admin_id, &*tx) - .await?; - let channel_visibility = channel::Entity::find() .filter(channel::Column::Id.eq(channel_id)) .one(&*tx) @@ -851,8 +932,11 @@ impl Database { ) -> Result> { let members = self .transaction(move |tx| async move { + self.check_user_is_channel_admin(channel_id, admin_id, &*tx) + .await?; + Ok(self - .get_channel_participant_details_internal(channel_id, admin_id, &*tx) + .get_channel_participant_details_internal(channel_id, &*tx) .await?) }) .await?; @@ -863,25 +947,18 @@ impl Database { .collect()) } - pub async fn get_channel_participants_internal( + pub async fn get_channel_participants( &self, - id: ChannelId, + channel_id: ChannelId, tx: &DatabaseTransaction, ) -> Result> { - let ancestor_ids = self.get_channel_ancestors(id, tx).await?; - let user_ids = channel_member::Entity::find() - .distinct() - .filter( - channel_member::Column::ChannelId - .is_in(ancestor_ids.iter().copied()) - .and(channel_member::Column::Accepted.eq(true)), - ) - .select_only() - .column(channel_member::Column::UserId) - .into_values::<_, QueryUserIds>() - .all(&*tx) + let participants = self + .get_channel_participant_details_internal(channel_id, &*tx) .await?; - Ok(user_ids) + Ok(participants + .into_iter() + .map(|member| member.user_id) + .collect()) } pub async fn check_user_is_channel_admin( @@ -951,18 +1028,12 @@ impl Database { Ok(row) } - // ordered from higher in tree to lower - // only considers one path to a channel - // includes the channel itself - pub async fn path_to_channel(&self, channel_id: ChannelId) -> Result> { - self.transaction(move |tx| async move { - Ok(self.path_to_channel_internal(channel_id, &*tx).await?) - }) - .await - } - - pub async fn parent_channel_id(&self, channel_id: ChannelId) -> Result> { - let path = self.path_to_channel(channel_id).await?; + pub async fn parent_channel_id( + &self, + channel_id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result> { + let path = self.path_to_channel(channel_id, &*tx).await?; if path.len() >= 2 { Ok(Some(path[path.len() - 2])) } else { @@ -973,8 +1044,9 @@ impl Database { pub async fn public_parent_channel_id( &self, channel_id: ChannelId, + tx: &DatabaseTransaction, ) -> Result> { - let path = self.path_to_channel(channel_id).await?; + let path = self.public_path_to_channel(channel_id, &*tx).await?; if path.len() >= 2 && path.last().copied() == Some(channel_id) { Ok(Some(path[path.len() - 2])) } else { @@ -982,7 +1054,7 @@ impl Database { } } - pub async fn path_to_channel_internal( + pub async fn path_to_channel( &self, channel_id: ChannelId, tx: &DatabaseTransaction, @@ -1005,27 +1077,12 @@ impl Database { .collect()) } - // ordered from higher in tree to lower - // only considers one path to a channel - // includes the channel itself - pub async fn public_path_to_channel(&self, channel_id: ChannelId) -> Result> { - self.transaction(move |tx| async move { - Ok(self - .public_path_to_channel_internal(channel_id, &*tx) - .await?) - }) - .await - } - - // ordered from higher in tree to lower - // only considers one path to a channel - // includes the channel itself - pub async fn public_path_to_channel_internal( + pub async fn public_path_to_channel( &self, channel_id: ChannelId, tx: &DatabaseTransaction, ) -> Result> { - let ancestor_ids = self.path_to_channel_internal(channel_id, &*tx).await?; + let ancestor_ids = self.path_to_channel(channel_id, &*tx).await?; let rows = channel::Entity::find() .filter(channel::Column::Id.is_in(ancestor_ids.iter().copied())) @@ -1151,27 +1208,6 @@ impl Database { Ok(channel_ids) } - // returns all ids of channels in the tree under this channel_id. - pub async fn get_channel_descendant_ids( - &self, - channel_id: ChannelId, - ) -> Result> { - self.transaction(|tx| async move { - let pairs = self.get_channel_descendants([channel_id], &*tx).await?; - - let mut results: HashSet = HashSet::default(); - for ChannelEdge { - parent_id: _, - channel_id, - } in pairs - { - results.insert(ChannelId::from_proto(channel_id)); - } - Ok(results) - }) - .await - } - // Returns the channel desendants as a sorted list of edges for further processing. // The edges are sorted such that you will see unknown channel ids as children // before you see them as parents. @@ -1388,9 +1424,6 @@ impl Database { from: ChannelId, ) -> Result<()> { self.transaction(|tx| async move { - // Note that even with these maxed permissions, this linking operation - // is still insecure because you can't remove someone's permissions to a - // channel if they've linked the channel to one where they're an admin. self.check_user_is_channel_admin(channel, user, &*tx) .await?; @@ -1433,6 +1466,8 @@ impl Database { .await? == 0; + dbg!(is_stranded, &paths); + // Make sure that there is always at least one path to the channel if is_stranded { let root_paths: Vec<_> = paths @@ -1445,6 +1480,8 @@ impl Database { } }) .collect(); + + dbg!(is_stranded, &root_paths); channel_path::Entity::insert_many(root_paths) .exec(&*tx) .await?; @@ -1453,49 +1490,64 @@ impl Database { Ok(()) } - /// Move a channel from one parent to another, returns the - /// Channels that were moved for notifying clients + /// Move a channel from one parent to another pub async fn move_channel( &self, - user: UserId, - channel: ChannelId, - from: ChannelId, - to: ChannelId, - ) -> Result { - if from == to { - return Ok(ChannelGraph { - channels: vec![], - edges: vec![], - }); - } - + channel_id: ChannelId, + old_parent_id: Option, + new_parent_id: ChannelId, + admin_id: UserId, + ) -> Result> { self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel, user, &*tx) + self.check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; - let moved_channels = self.link_channel_internal(user, channel, to, &*tx).await?; + debug_assert_eq!( + self.parent_channel_id(channel_id, &*tx).await?, + old_parent_id + ); - self.unlink_channel_internal(user, channel, from, &*tx) + if old_parent_id == Some(new_parent_id) { + return Ok(None); + } + let previous_participants = self + .get_channel_participant_details_internal(channel_id, &*tx) .await?; - Ok(moved_channels) - }) - .await - } + self.link_channel_internal(admin_id, channel_id, new_parent_id, &*tx) + .await?; - pub async fn assert_root_channel(&self, channel: ChannelId) -> Result<()> { - self.transaction(|tx| async move { - let path = channel_path::Entity::find() - .filter(channel_path::Column::ChannelId.eq(channel)) - .one(&*tx) + if let Some(from) = old_parent_id { + self.unlink_channel_internal(admin_id, channel_id, from, &*tx) + .await?; + } + + let participants_to_update: HashMap = self + .participants_to_notify_for_channel_change(new_parent_id, &*tx) .await? - .ok_or_else(|| anyhow!("no such channel found"))?; + .into_iter() + .collect(); - let mut id_parts = path.id_path.trim_matches('/').split('/'); + let mut moved_channels: HashSet = HashSet::default(); + moved_channels.insert(channel_id); + for edge in self.get_channel_descendants([channel_id], &*tx).await? { + moved_channels.insert(ChannelId::from_proto(edge.channel_id)); + } - (id_parts.next().is_some() && id_parts.next().is_none()) - .then_some(()) - .ok_or_else(|| anyhow!("channel is not a root channel").into()) + let mut participants_to_remove: HashSet = HashSet::default(); + for participant in previous_participants { + if participant.kind == proto::channel_member::Kind::AncestorMember { + if !participants_to_update.contains_key(&participant.user_id) { + participants_to_remove.insert(participant.user_id); + } + } + } + + Ok(Some(MoveChannelResult { + participants_to_remove, + participants_to_update, + moved_channels, + })) }) .await } diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index de7334425f..4bcac025c5 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -183,9 +183,7 @@ impl Database { ) .await?; - let mut channel_members = self - .get_channel_participants_internal(channel_id, &*tx) - .await?; + let mut channel_members = self.get_channel_participants(channel_id, &*tx).await?; channel_members.retain(|member| !participant_user_ids.contains(member)); Ok(( diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index d2120495b0..630d51cfe6 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -53,9 +53,7 @@ impl Database { let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; let channel_members; if let Some(channel_id) = channel_id { - channel_members = self - .get_channel_participants_internal(channel_id, &tx) - .await?; + channel_members = self.get_channel_participants(channel_id, &tx).await?; } else { channel_members = Vec::new(); @@ -423,9 +421,7 @@ impl Database { .await?; let room = self.get_room(room_id, &tx).await?; - let channel_members = self - .get_channel_participants_internal(channel_id, &tx) - .await?; + let channel_members = self.get_channel_participants(channel_id, &tx).await?; Ok(JoinRoom { room, channel_id: Some(channel_id), @@ -724,8 +720,7 @@ impl Database { let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_participants_internal(channel_id, &tx) - .await? + self.get_channel_participants(channel_id, &tx).await? } else { Vec::new() }; @@ -883,8 +878,7 @@ impl Database { }; let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_participants_internal(channel_id, &tx) - .await? + self.get_channel_participants(channel_id, &tx).await? } else { Vec::new() }; diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index a323f2919e..1767a773ff 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -36,28 +36,28 @@ async fn test_channels(db: &Arc) { .await .unwrap(); - let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap(); + let crdb_id = db.create_sub_channel("crdb", zed_id, a_id).await.unwrap(); let livestreaming_id = db - .create_channel("livestreaming", Some(zed_id), a_id) + .create_sub_channel("livestreaming", zed_id, a_id) .await .unwrap(); let replace_id = db - .create_channel("replace", Some(zed_id), a_id) + .create_sub_channel("replace", zed_id, a_id) .await .unwrap(); - let mut members = db.get_channel_members(replace_id).await.unwrap(); + let mut members = db + .transaction(|tx| async move { Ok(db.get_channel_participants(replace_id, &*tx).await?) }) + .await + .unwrap(); members.sort(); assert_eq!(members, &[a_id, b_id]); let rust_id = db.create_root_channel("rust", a_id).await.unwrap(); - let cargo_id = db - .create_channel("cargo", Some(rust_id), a_id) - .await - .unwrap(); + let cargo_id = db.create_sub_channel("cargo", rust_id, a_id).await.unwrap(); let cargo_ra_id = db - .create_channel("cargo-ra", Some(cargo_id), a_id) + .create_sub_channel("cargo-ra", cargo_id, a_id) .await .unwrap(); @@ -264,7 +264,7 @@ async fn test_channel_invites(db: &Arc) { .unwrap(); let channel_1_3 = db - .create_channel("channel_3", Some(channel_1_1), user_1) + .create_sub_channel("channel_3", channel_1_1, user_1) .await .unwrap(); @@ -277,7 +277,7 @@ async fn test_channel_invites(db: &Arc) { &[ proto::ChannelMember { user_id: user_1.to_proto(), - kind: proto::channel_member::Kind::Member.into(), + kind: proto::channel_member::Kind::AncestorMember.into(), role: proto::ChannelRole::Admin.into(), }, proto::ChannelMember { @@ -369,20 +369,17 @@ async fn test_db_channel_moving(db: &Arc) { let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); - let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap(); + let crdb_id = db.create_sub_channel("crdb", zed_id, a_id).await.unwrap(); - let gpui2_id = db - .create_channel("gpui2", Some(zed_id), a_id) - .await - .unwrap(); + let gpui2_id = db.create_sub_channel("gpui2", zed_id, a_id).await.unwrap(); let livestreaming_id = db - .create_channel("livestreaming", Some(crdb_id), a_id) + .create_sub_channel("livestreaming", crdb_id, a_id) .await .unwrap(); let livestreaming_dag_id = db - .create_channel("livestreaming_dag", Some(livestreaming_id), a_id) + .create_sub_channel("livestreaming_dag", livestreaming_id, a_id) .await .unwrap(); @@ -409,311 +406,311 @@ async fn test_db_channel_moving(db: &Arc) { .await .is_err()); - // ======================================================================== - // Make a link - db.link_channel(a_id, livestreaming_id, zed_id) - .await - .unwrap(); + // // ======================================================================== + // // Make a link + // db.link_channel(a_id, livestreaming_id, zed_id) + // .await + // .unwrap(); - // DAG is now: - // /- gpui2 - // zed -- crdb - livestreaming - livestreaming_dag - // \---------/ - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - ], - ); + // // DAG is now: + // // /- gpui2 + // // zed -- crdb - livestreaming - livestreaming_dag + // // \---------/ + // let result = db.get_channels_for_user(a_id).await.unwrap(); + // assert_dag( + // result.channels, + // &[ + // (zed_id, None), + // (crdb_id, Some(zed_id)), + // (gpui2_id, Some(zed_id)), + // (livestreaming_id, Some(zed_id)), + // (livestreaming_id, Some(crdb_id)), + // (livestreaming_dag_id, Some(livestreaming_id)), + // ], + // ); - // ======================================================================== - // Create a new channel below a channel with multiple parents - let livestreaming_dag_sub_id = db - .create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), a_id) - .await - .unwrap(); + // // ======================================================================== + // // Create a new channel below a channel with multiple parents + // let livestreaming_dag_sub_id = db + // .create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), a_id) + // .await + // .unwrap(); - // DAG is now: - // /- gpui2 - // zed -- crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id - // \---------/ - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); + // // DAG is now: + // // /- gpui2 + // // zed -- crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id + // // \---------/ + // let result = db.get_channels_for_user(a_id).await.unwrap(); + // assert_dag( + // result.channels, + // &[ + // (zed_id, None), + // (crdb_id, Some(zed_id)), + // (gpui2_id, Some(zed_id)), + // (livestreaming_id, Some(zed_id)), + // (livestreaming_id, Some(crdb_id)), + // (livestreaming_dag_id, Some(livestreaming_id)), + // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + // ], + // ); - // ======================================================================== - // Test a complex DAG by making another link - let returned_channels = db - .link_channel(a_id, livestreaming_dag_sub_id, livestreaming_id) - .await - .unwrap(); + // // ======================================================================== + // // Test a complex DAG by making another link + // let returned_channels = db + // .link_channel(a_id, livestreaming_dag_sub_id, livestreaming_id) + // .await + // .unwrap(); - // DAG is now: - // /- gpui2 /---------------------\ - // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id - // \--------/ + // // DAG is now: + // // /- gpui2 /---------------------\ + // // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id + // // \--------/ - // make sure we're getting just the new link - // Not using the assert_dag helper because we want to make sure we're returning the full data - pretty_assertions::assert_eq!( - returned_channels, - graph( - &[( - livestreaming_dag_sub_id, - "livestreaming_dag_sub", - ChannelRole::Admin - )], - &[(livestreaming_dag_sub_id, livestreaming_id)] - ) - ); + // // make sure we're getting just the new link + // // Not using the assert_dag helper because we want to make sure we're returning the full data + // pretty_assertions::assert_eq!( + // returned_channels, + // graph( + // &[( + // livestreaming_dag_sub_id, + // "livestreaming_dag_sub", + // ChannelRole::Admin + // )], + // &[(livestreaming_dag_sub_id, livestreaming_id)] + // ) + // ); - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); + // let result = db.get_channels_for_user(a_id).await.unwrap(); + // assert_dag( + // result.channels, + // &[ + // (zed_id, None), + // (crdb_id, Some(zed_id)), + // (gpui2_id, Some(zed_id)), + // (livestreaming_id, Some(zed_id)), + // (livestreaming_id, Some(crdb_id)), + // (livestreaming_dag_id, Some(livestreaming_id)), + // (livestreaming_dag_sub_id, Some(livestreaming_id)), + // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + // ], + // ); - // ======================================================================== - // Test a complex DAG by making another link - let returned_channels = db - .link_channel(a_id, livestreaming_id, gpui2_id) - .await - .unwrap(); + // // ======================================================================== + // // Test a complex DAG by making another link + // let returned_channels = db + // .link_channel(a_id, livestreaming_id, gpui2_id) + // .await + // .unwrap(); - // DAG is now: - // /- gpui2 -\ /---------------------\ - // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub_id - // \---------/ + // // DAG is now: + // // /- gpui2 -\ /---------------------\ + // // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub_id + // // \---------/ - // Make sure that we're correctly getting the full sub-dag - pretty_assertions::assert_eq!( - returned_channels, - graph( - &[ - (livestreaming_id, "livestreaming", ChannelRole::Admin), - ( - livestreaming_dag_id, - "livestreaming_dag", - ChannelRole::Admin - ), - ( - livestreaming_dag_sub_id, - "livestreaming_dag_sub", - ChannelRole::Admin - ), - ], - &[ - (livestreaming_id, gpui2_id), - (livestreaming_dag_id, livestreaming_id), - (livestreaming_dag_sub_id, livestreaming_id), - (livestreaming_dag_sub_id, livestreaming_dag_id), - ] - ) - ); + // // Make sure that we're correctly getting the full sub-dag + // pretty_assertions::assert_eq!( + // returned_channels, + // graph( + // &[ + // (livestreaming_id, "livestreaming", ChannelRole::Admin), + // ( + // livestreaming_dag_id, + // "livestreaming_dag", + // ChannelRole::Admin + // ), + // ( + // livestreaming_dag_sub_id, + // "livestreaming_dag_sub", + // ChannelRole::Admin + // ), + // ], + // &[ + // (livestreaming_id, gpui2_id), + // (livestreaming_dag_id, livestreaming_id), + // (livestreaming_dag_sub_id, livestreaming_id), + // (livestreaming_dag_sub_id, livestreaming_dag_id), + // ] + // ) + // ); - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_id, Some(gpui2_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); + // let result = db.get_channels_for_user(a_id).await.unwrap(); + // assert_dag( + // result.channels, + // &[ + // (zed_id, None), + // (crdb_id, Some(zed_id)), + // (gpui2_id, Some(zed_id)), + // (livestreaming_id, Some(zed_id)), + // (livestreaming_id, Some(crdb_id)), + // (livestreaming_id, Some(gpui2_id)), + // (livestreaming_dag_id, Some(livestreaming_id)), + // (livestreaming_dag_sub_id, Some(livestreaming_id)), + // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + // ], + // ); - // ======================================================================== - // Test unlinking in a complex DAG by removing the inner link - db.unlink_channel(a_id, livestreaming_dag_sub_id, livestreaming_id) - .await - .unwrap(); + // // ======================================================================== + // // Test unlinking in a complex DAG by removing the inner link + // db.unlink_channel(a_id, livestreaming_dag_sub_id, livestreaming_id) + // .await + // .unwrap(); - // DAG is now: - // /- gpui2 -\ - // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub - // \---------/ + // // DAG is now: + // // /- gpui2 -\ + // // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub + // // \---------/ - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(gpui2_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); + // let result = db.get_channels_for_user(a_id).await.unwrap(); + // assert_dag( + // result.channels, + // &[ + // (zed_id, None), + // (crdb_id, Some(zed_id)), + // (gpui2_id, Some(zed_id)), + // (livestreaming_id, Some(gpui2_id)), + // (livestreaming_id, Some(zed_id)), + // (livestreaming_id, Some(crdb_id)), + // (livestreaming_dag_id, Some(livestreaming_id)), + // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + // ], + // ); - // ======================================================================== - // Test unlinking in a complex DAG by removing the inner link - db.unlink_channel(a_id, livestreaming_id, gpui2_id) - .await - .unwrap(); + // // ======================================================================== + // // Test unlinking in a complex DAG by removing the inner link + // db.unlink_channel(a_id, livestreaming_id, gpui2_id) + // .await + // .unwrap(); - // DAG is now: - // /- gpui2 - // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub - // \---------/ - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); + // // DAG is now: + // // /- gpui2 + // // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub + // // \---------/ + // let result = db.get_channels_for_user(a_id).await.unwrap(); + // assert_dag( + // result.channels, + // &[ + // (zed_id, None), + // (crdb_id, Some(zed_id)), + // (gpui2_id, Some(zed_id)), + // (livestreaming_id, Some(zed_id)), + // (livestreaming_id, Some(crdb_id)), + // (livestreaming_dag_id, Some(livestreaming_id)), + // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + // ], + // ); - // ======================================================================== - // Test moving DAG nodes by moving livestreaming to be below gpui2 - db.move_channel(a_id, livestreaming_id, crdb_id, gpui2_id) - .await - .unwrap(); + // // ======================================================================== + // // Test moving DAG nodes by moving livestreaming to be below gpui2 + // db.move_channel(livestreaming_id, Some(crdb_id), gpui2_id, a_id) + // .await + // .unwrap(); - // DAG is now: - // /- gpui2 -- livestreaming - livestreaming_dag - livestreaming_dag_sub - // zed - crdb / - // \---------/ - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(gpui2_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); + // // DAG is now: + // // /- gpui2 -- livestreaming - livestreaming_dag - livestreaming_dag_sub + // // zed - crdb / + // // \---------/ + // let result = db.get_channels_for_user(a_id).await.unwrap(); + // assert_dag( + // result.channels, + // &[ + // (zed_id, None), + // (crdb_id, Some(zed_id)), + // (gpui2_id, Some(zed_id)), + // (livestreaming_id, Some(zed_id)), + // (livestreaming_id, Some(gpui2_id)), + // (livestreaming_dag_id, Some(livestreaming_id)), + // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + // ], + // ); - // ======================================================================== - // Deleting a channel should not delete children that still have other parents - db.delete_channel(gpui2_id, a_id).await.unwrap(); + // // ======================================================================== + // // Deleting a channel should not delete children that still have other parents + // db.delete_channel(gpui2_id, a_id).await.unwrap(); - // DAG is now: - // zed - crdb - // \- livestreaming - livestreaming_dag - livestreaming_dag_sub - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); + // // DAG is now: + // // zed - crdb + // // \- livestreaming - livestreaming_dag - livestreaming_dag_sub + // let result = db.get_channels_for_user(a_id).await.unwrap(); + // assert_dag( + // result.channels, + // &[ + // (zed_id, None), + // (crdb_id, Some(zed_id)), + // (livestreaming_id, Some(zed_id)), + // (livestreaming_dag_id, Some(livestreaming_id)), + // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + // ], + // ); - // ======================================================================== - // Unlinking a channel from it's parent should automatically promote it to a root channel - db.unlink_channel(a_id, crdb_id, zed_id).await.unwrap(); + // // ======================================================================== + // // Unlinking a channel from it's parent should automatically promote it to a root channel + // db.unlink_channel(a_id, crdb_id, zed_id).await.unwrap(); - // DAG is now: - // crdb - // zed - // \- livestreaming - livestreaming_dag - livestreaming_dag_sub + // // DAG is now: + // // crdb + // // zed + // // \- livestreaming - livestreaming_dag - livestreaming_dag_sub - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, None), - (livestreaming_id, Some(zed_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); + // let result = db.get_channels_for_user(a_id).await.unwrap(); + // assert_dag( + // result.channels, + // &[ + // (zed_id, None), + // (crdb_id, None), + // (livestreaming_id, Some(zed_id)), + // (livestreaming_dag_id, Some(livestreaming_id)), + // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + // ], + // ); - // ======================================================================== - // You should be able to move a root channel into a non-root channel - db.link_channel(a_id, crdb_id, zed_id).await.unwrap(); + // // ======================================================================== + // // You should be able to move a root channel into a non-root channel + // db.link_channel(a_id, crdb_id, zed_id).await.unwrap(); - // DAG is now: - // zed - crdb - // \- livestreaming - livestreaming_dag - livestreaming_dag_sub + // // DAG is now: + // // zed - crdb + // // \- livestreaming - livestreaming_dag - livestreaming_dag_sub - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); + // let result = db.get_channels_for_user(a_id).await.unwrap(); + // assert_dag( + // result.channels, + // &[ + // (zed_id, None), + // (crdb_id, Some(zed_id)), + // (livestreaming_id, Some(zed_id)), + // (livestreaming_dag_id, Some(livestreaming_id)), + // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + // ], + // ); - // ======================================================================== - // Prep for DAG deletion test - db.link_channel(a_id, livestreaming_id, crdb_id) - .await - .unwrap(); + // // ======================================================================== + // // Prep for DAG deletion test + // db.link_channel(a_id, livestreaming_id, crdb_id) + // .await + // .unwrap(); - // DAG is now: - // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub - // \--------/ + // // DAG is now: + // // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub + // // \--------/ - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); + // let result = db.get_channels_for_user(a_id).await.unwrap(); + // assert_dag( + // result.channels, + // &[ + // (zed_id, None), + // (crdb_id, Some(zed_id)), + // (livestreaming_id, Some(zed_id)), + // (livestreaming_id, Some(crdb_id)), + // (livestreaming_dag_id, Some(livestreaming_id)), + // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), + // ], + // ); - // Deleting the parent of a DAG should delete the whole DAG: - db.delete_channel(zed_id, a_id).await.unwrap(); - let result = db.get_channels_for_user(a_id).await.unwrap(); + // // Deleting the parent of a DAG should delete the whole DAG: + // db.delete_channel(zed_id, a_id).await.unwrap(); + // let result = db.get_channels_for_user(a_id).await.unwrap(); - assert!(result.channels.is_empty()) + // assert!(result.channels.is_empty()) } test_both_dbs!( @@ -740,12 +737,12 @@ async fn test_db_channel_moving_bugs(db: &Arc) { let zed_id = db.create_root_channel("zed", user_id).await.unwrap(); let projects_id = db - .create_channel("projects", Some(zed_id), user_id) + .create_sub_channel("projects", zed_id, user_id) .await .unwrap(); let livestreaming_id = db - .create_channel("livestreaming", Some(projects_id), user_id) + .create_sub_channel("livestreaming", projects_id, user_id) .await .unwrap(); @@ -753,25 +750,37 @@ async fn test_db_channel_moving_bugs(db: &Arc) { // Move to same parent should be a no-op assert!(db - .move_channel(user_id, projects_id, zed_id, zed_id) + .move_channel(projects_id, Some(zed_id), zed_id, user_id) .await .unwrap() - .is_empty()); - - // Stranding a channel should retain it's sub channels - db.unlink_channel(user_id, projects_id, zed_id) - .await - .unwrap(); + .is_none()); let result = db.get_channels_for_user(user_id).await.unwrap(); assert_dag( result.channels, &[ (zed_id, None), - (projects_id, None), + (projects_id, Some(zed_id)), (livestreaming_id, Some(projects_id)), ], ); + + // Stranding a channel should retain it's sub channels + // Commented out as we don't fix permissions when this happens yet. + // + // db.unlink_channel(user_id, projects_id, zed_id) + // .await + // .unwrap(); + + // let result = db.get_channels_for_user(user_id).await.unwrap(); + // assert_dag( + // result.channels, + // &[ + // (zed_id, None), + // (projects_id, None), + // (livestreaming_id, Some(projects_id)), + // ], + // ); } test_both_dbs!( @@ -787,11 +796,11 @@ async fn test_user_is_channel_participant(db: &Arc) { let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); let active_channel = db - .create_channel("active", Some(zed_channel), admin) + .create_sub_channel("active", zed_channel, admin) .await .unwrap(); let vim_channel = db - .create_channel("vim", Some(active_channel), admin) + .create_sub_channel("vim", active_channel, admin) .await .unwrap(); @@ -834,7 +843,7 @@ async fn test_user_is_channel_participant(db: &Arc) { &[ proto::ChannelMember { user_id: admin.to_proto(), - kind: proto::channel_member::Kind::Member.into(), + kind: proto::channel_member::Kind::AncestorMember.into(), role: proto::ChannelRole::Admin.into(), }, proto::ChannelMember { @@ -892,7 +901,7 @@ async fn test_user_is_channel_participant(db: &Arc) { &[ proto::ChannelMember { user_id: admin.to_proto(), - kind: proto::channel_member::Kind::Member.into(), + kind: proto::channel_member::Kind::AncestorMember.into(), role: proto::ChannelRole::Admin.into(), }, proto::ChannelMember { @@ -933,7 +942,7 @@ async fn test_user_is_channel_participant(db: &Arc) { &[ proto::ChannelMember { user_id: admin.to_proto(), - kind: proto::channel_member::Kind::Member.into(), + kind: proto::channel_member::Kind::AncestorMember.into(), role: proto::ChannelRole::Admin.into(), }, proto::ChannelMember { @@ -981,7 +990,7 @@ async fn test_user_is_channel_participant(db: &Arc) { &[ proto::ChannelMember { user_id: admin.to_proto(), - kind: proto::channel_member::Kind::Member.into(), + kind: proto::channel_member::Kind::AncestorMember.into(), role: proto::ChannelRole::Admin.into(), }, proto::ChannelMember { @@ -1016,17 +1025,17 @@ async fn test_user_joins_correct_channel(db: &Arc) { let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); let active_channel = db - .create_channel("active", Some(zed_channel), admin) + .create_sub_channel("active", zed_channel, admin) .await .unwrap(); let vim_channel = db - .create_channel("vim", Some(active_channel), admin) + .create_sub_channel("vim", active_channel, admin) .await .unwrap(); let vim2_channel = db - .create_channel("vim2", Some(vim_channel), admin) + .create_sub_channel("vim2", vim_channel, admin) .await .unwrap(); @@ -1043,11 +1052,15 @@ async fn test_user_joins_correct_channel(db: &Arc) { .unwrap(); let most_public = db - .public_path_to_channel(vim_channel) + .transaction(|tx| async move { + Ok(db + .public_path_to_channel(vim_channel, &tx) + .await? + .first() + .cloned()) + }) .await - .unwrap() - .first() - .cloned(); + .unwrap(); assert_eq!(most_public, Some(zed_channel)) } diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs index 272d8e0100..f9d97e2181 100644 --- a/crates/collab/src/db/tests/message_tests.rs +++ b/crates/collab/src/db/tests/message_tests.rs @@ -25,7 +25,7 @@ async fn test_channel_message_retrieval(db: &Arc) { .await .unwrap() .user_id; - let channel = db.create_channel("channel", None, user).await.unwrap(); + let channel = db.create_root_channel("channel", user).await.unwrap(); let owner_id = db.create_server("test").await.unwrap().0 as u32; db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user) @@ -87,7 +87,7 @@ async fn test_channel_message_nonces(db: &Arc) { .await .unwrap() .user_id; - let channel = db.create_channel("channel", None, user).await.unwrap(); + let channel = db.create_root_channel("channel", user).await.unwrap(); let owner_id = db.create_server("test").await.unwrap().0 as u32; @@ -151,9 +151,9 @@ async fn test_channel_message_new_notification(db: &Arc) { .unwrap() .user_id; - let channel_1 = db.create_channel("channel", None, user).await.unwrap(); + let channel_1 = db.create_root_channel("channel", user).await.unwrap(); - let channel_2 = db.create_channel("channel-2", None, user).await.unwrap(); + let channel_2 = db.create_root_channel("channel-2", user).await.unwrap(); db.invite_channel_member(channel_1, observer, user, ChannelRole::Member) .await diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 18fbc0d7bc..f8648a2b14 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -3,8 +3,9 @@ mod connection_pool; use crate::{ auth, db::{ - self, BufferId, ChannelId, ChannelRole, ChannelVisibility, ChannelsForUser, Database, - MessageId, ProjectId, RoomId, ServerId, User, UserId, + self, BufferId, ChannelId, ChannelsForUser, CreateChannelResult, Database, MessageId, + MoveChannelResult, ProjectId, RenameChannelResult, RoomId, ServerId, + SetChannelVisibilityResult, User, UserId, }, executor::Executor, AppState, Result, @@ -590,7 +591,7 @@ impl Server { let mut pool = this.connection_pool.lock(); pool.add_connection(connection_id, user_id, user.admin); this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; - this.peer.send(connection_id, build_initial_channels_update( + this.peer.send(connection_id, build_channels_update( channels_for_user, channel_invites ))?; @@ -2202,31 +2203,21 @@ async fn create_channel( let db = session.db().await; let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id)); - let id = db + let CreateChannelResult { + channel, + participants_to_update, + } = db .create_channel(&request.name, parent_id, session.user_id) .await?; response.send(proto::CreateChannelResponse { - channel: Some(proto::Channel { - id: id.to_proto(), - name: request.name, - visibility: proto::ChannelVisibility::Members as i32, - role: proto::ChannelRole::Admin.into(), - }), + channel: Some(channel.to_proto()), parent_id: request.parent_id, })?; - let Some(parent_id) = parent_id else { - return Ok(()); - }; - - let updates = db - .participants_to_notify_for_channel_change(parent_id, session.user_id) - .await?; - let connection_pool = session.connection_pool().await; - for (user_id, channels) in updates { - let update = build_initial_channels_update(channels, vec![]); + for (user_id, channels) in participants_to_update { + let update = build_channels_update(channels, vec![]); for connection_id in connection_pool.user_connection_ids(user_id) { if user_id == session.user_id { continue; @@ -2340,49 +2331,21 @@ async fn set_channel_visibility( let channel_id = ChannelId::from_proto(request.channel_id); let visibility = request.visibility().into(); - let previous_members = db - .get_channel_participant_details(channel_id, session.user_id) + let SetChannelVisibilityResult { + participants_to_update, + participants_to_remove, + } = db + .set_channel_visibility(channel_id, visibility, session.user_id) .await?; - db.set_channel_visibility(channel_id, visibility, session.user_id) - .await?; - - let mut updates: HashMap = db - .participants_to_notify_for_channel_change(channel_id, session.user_id) - .await? - .into_iter() - .collect(); - - let mut participants_who_lost_access: HashSet = HashSet::default(); - match visibility { - ChannelVisibility::Members => { - for member in previous_members { - if ChannelRole::from(member.role()).can_only_see_public_descendants() { - participants_who_lost_access.insert(UserId::from_proto(member.user_id)); - } - } - } - ChannelVisibility::Public => { - if let Some(public_parent_id) = db.public_parent_channel_id(channel_id).await? { - let parent_updates = db - .participants_to_notify_for_channel_change(public_parent_id, session.user_id) - .await?; - - for (user_id, channels) in parent_updates { - updates.insert(user_id, channels); - } - } - } - } - let connection_pool = session.connection_pool().await; - for (user_id, channels) in updates { - let update = build_initial_channels_update(channels, vec![]); + for (user_id, channels) in participants_to_update { + let update = build_channels_update(channels, vec![]); for connection_id in connection_pool.user_connection_ids(user_id) { session.peer.send(connection_id, update.clone())?; } } - for user_id in participants_who_lost_access { + for user_id in participants_to_remove { let update = proto::UpdateChannels { delete_channels: vec![channel_id.to_proto()], ..Default::default() @@ -2416,7 +2379,7 @@ async fn set_channel_member_role( let mut update = proto::UpdateChannels::default(); if channel_member.accepted { let channels = db.get_channel_for_user(channel_id, member_id).await?; - update = build_initial_channels_update(channels, vec![]); + update = build_channels_update(channels, vec![]); } else { let channel = db.get_channel(channel_id, session.user_id).await?; update.channel_invitations.push(proto::Channel { @@ -2446,34 +2409,24 @@ async fn rename_channel( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let channel = db + let RenameChannelResult { + channel, + participants_to_update, + } = db .rename_channel(channel_id, session.user_id, &request.name) .await?; response.send(proto::RenameChannelResponse { - channel: Some(proto::Channel { - id: channel.id.to_proto(), - name: channel.name.clone(), - visibility: channel.visibility.into(), - role: proto::ChannelRole::Admin.into(), - }), + channel: Some(channel.to_proto()), })?; - let members = db - .get_channel_participant_details(channel_id, session.user_id) - .await?; - let connection_pool = session.connection_pool().await; - for member in members { - for connection_id in connection_pool.user_connection_ids(UserId::from_proto(member.user_id)) - { - let mut update = proto::UpdateChannels::default(); - update.channels.push(proto::Channel { - id: channel.id.to_proto(), - name: channel.name.clone(), - visibility: channel.visibility.into(), - role: member.role.into(), - }); + for (user_id, channel) in participants_to_update { + for connection_id in connection_pool.user_connection_ids(user_id) { + let update = proto::UpdateChannels { + channels: vec![channel.to_proto()], + ..Default::default() + }; session.peer.send(connection_id, update.clone())?; } @@ -2493,25 +2446,12 @@ async fn link_channel( let channel_id = ChannelId::from_proto(request.channel_id); let to = ChannelId::from_proto(request.to); - // TODO: Remove this restriction once we have symlinks - db.assert_root_channel(channel_id).await?; - - db.link_channel(session.user_id, channel_id, to).await?; - - let member_updates = db - .participants_to_notify_for_channel_change(to, session.user_id) + let result = db + .move_channel(channel_id, None, to, session.user_id) .await?; + drop(db); - dbg!(&member_updates); - - let connection_pool = session.connection_pool().await; - - for (member_id, channels) in member_updates { - let update = build_initial_channels_update(channels, vec![]); - for connection_id in connection_pool.user_connection_ids(member_id) { - session.peer.send(connection_id, update.clone())?; - } - } + notify_channel_moved(result, session).await?; response.send(Ack {})?; @@ -2537,64 +2477,46 @@ async fn move_channel( let from_parent = ChannelId::from_proto(request.from); let to = ChannelId::from_proto(request.to); - let previous_participants = db - .get_channel_participant_details(channel_id, session.user_id) + let result = db + .move_channel(channel_id, Some(from_parent), to, session.user_id) .await?; + drop(db); - debug_assert_eq!(db.parent_channel_id(channel_id).await?, Some(from_parent)); + notify_channel_moved(result, session).await?; - let channels_to_send = db - .move_channel(session.user_id, channel_id, from_parent, to) - .await?; + response.send(Ack {})?; + Ok(()) +} - if channels_to_send.is_empty() { - response.send(Ack {})?; +async fn notify_channel_moved(result: Option, session: Session) -> Result<()> { + let Some(MoveChannelResult { + participants_to_remove, + participants_to_update, + moved_channels, + }) = result + else { return Ok(()); - } - - let updates = db - .participants_to_notify_for_channel_change(to, session.user_id) - .await?; - - let mut participants_who_lost_access: HashSet = HashSet::default(); - let mut channels_to_delete = db.get_channel_descendant_ids(channel_id).await?; - channels_to_delete.insert(channel_id); - - for previous_participant in previous_participants.iter() { - let user_id = UserId::from_proto(previous_participant.user_id); - if previous_participant.kind() == proto::channel_member::Kind::AncestorMember { - participants_who_lost_access.insert(user_id); - } - } + }; + let moved_channels: Vec = moved_channels.iter().map(|id| id.to_proto()).collect(); let connection_pool = session.connection_pool().await; - for (user_id, channels) in updates { - let mut update = build_initial_channels_update(channels, vec![]); - update.delete_channels = channels_to_delete - .iter() - .map(|channel_id| channel_id.to_proto()) - .collect(); - participants_who_lost_access.remove(&user_id); + for (user_id, channels) in participants_to_update { + let mut update = build_channels_update(channels, vec![]); + update.delete_channels = moved_channels.clone(); for connection_id in connection_pool.user_connection_ids(user_id) { session.peer.send(connection_id, update.clone())?; } } - for user_id in participants_who_lost_access { + for user_id in participants_to_remove { let update = proto::UpdateChannels { - delete_channels: channels_to_delete - .iter() - .map(|channel_id| channel_id.to_proto()) - .collect(), + delete_channels: moved_channels.clone(), ..Default::default() }; for connection_id in connection_pool.user_connection_ids(user_id) { session.peer.send(connection_id, update.clone())?; } } - - response.send(Ack {})?; - Ok(()) } @@ -2641,38 +2563,12 @@ async fn channel_membership_updated( channel_id: ChannelId, session: &Session, ) -> Result<(), crate::Error> { - let mut update = proto::UpdateChannels::default(); + let result = db.get_channel_for_user(channel_id, session.user_id).await?; + let mut update = build_channels_update(result, vec![]); update .remove_channel_invitations .push(channel_id.to_proto()); - let result = db.get_channel_for_user(channel_id, session.user_id).await?; - update.channels.extend( - result - .channels - .channels - .into_iter() - .map(|channel| proto::Channel { - id: channel.id.to_proto(), - visibility: channel.visibility.into(), - role: channel.role.into(), - name: channel.name, - }), - ); - update.unseen_channel_messages = result.channel_messages; - update.unseen_channel_buffer_changes = result.unseen_buffer_changes; - update.insert_edge = result.channels.edges; - update - .channel_participants - .extend( - result - .channel_participants - .into_iter() - .map(|(channel_id, user_ids)| proto::ChannelParticipants { - channel_id: channel_id.to_proto(), - participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(), - }), - ); session.peer.send(session.connection_id, update)?; Ok(()) } @@ -3155,7 +3051,7 @@ fn to_tungstenite_message(message: AxumMessage) -> TungsteniteMessage { } } -fn build_initial_channels_update( +fn build_channels_update( channels: ChannelsForUser, channel_invites: Vec, ) -> proto::UpdateChannels { diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index 1b24c7a3d2..f4eca6b5ab 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -48,7 +48,7 @@ impl RandomizedTest for RandomChannelBufferTest { let db = &server.app_state.db; for ix in 0..CHANNEL_COUNT { let id = db - .create_channel(&format!("channel-{ix}"), None, users[0].user_id) + .create_root_channel(&format!("channel-{ix}"), users[0].user_id) .await .unwrap(); for user in &users[1..] { diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index c37ea19d52..81d86943fc 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -604,38 +604,6 @@ impl TestClient { ) -> WindowHandle { cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) } - - pub async fn add_admin_to_channel( - &self, - user: (&TestClient, &mut TestAppContext), - channel: u64, - cx_self: &mut TestAppContext, - ) { - let (other_client, other_cx) = user; - - cx_self - .read(ChannelStore::global) - .update(cx_self, |channel_store, cx| { - channel_store.invite_member( - channel, - other_client.user_id().unwrap(), - ChannelRole::Admin, - cx, - ) - }) - .await - .unwrap(); - - cx_self.foreground().run_until_parked(); - - other_cx - .read(ChannelStore::global) - .update(other_cx, |channel_store, _| { - channel_store.respond_to_channel_invite(channel, true) - }) - .await - .unwrap(); - } } impl Drop for TestClient { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 70ea87cfdd..1d1092be6e 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2662,7 +2662,6 @@ impl CollabPanel { location: path.clone(), }, ), - ContextMenuItem::Separator, ContextMenuItem::action( "Move this channel", StartMoveChannelFor { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 1bf54dedb7..20a47954a4 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -970,16 +970,10 @@ message UpdateChannels { repeated Channel channel_invitations = 5; repeated uint64 remove_channel_invitations = 6; repeated ChannelParticipants channel_participants = 7; - //repeated ChannelRoles channel_roles = 8; repeated UnseenChannelMessage unseen_channel_messages = 9; repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10; } -//message ChannelRoles { -// ChannelRole role = 1; -// uint64 channel_id = 2; -//} - message UnseenChannelMessage { uint64 channel_id = 1; uint64 message_id = 2; From 0eff7c6ca93267da4502bbc13ae0e4b8ca1c6e0b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 19 Oct 2023 13:03:44 -0600 Subject: [PATCH 07/40] Add read-only channel notes support Fix some bugs where ChannelNotes and ChannelChat had old cached channel instances --- Cargo.lock | 1 + assets/keymaps/default.json | 198 ++++-------------- crates/call/src/room.rs | 2 +- crates/channel/src/channel_buffer.rs | 28 ++- crates/channel/src/channel_chat.rs | 27 +-- crates/channel/src/channel_store.rs | 48 ++++- .../src/channel_store/channel_index.rs | 21 +- .../collab/src/tests/channel_buffer_tests.rs | 4 +- .../src/tests/random_channel_buffer_tests.rs | 13 +- crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/channel_view.rs | 46 +++- crates/collab_ui/src/chat_panel.rs | 16 +- 12 files changed, 186 insertions(+), 219 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ecbe076711..d827b314c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1569,6 +1569,7 @@ dependencies = [ "serde", "serde_derive", "settings", + "smallvec", "theme", "theme_selector", "time", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 8422d53abc..ef6a655bdc 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -370,42 +370,15 @@ { "context": "Pane", "bindings": { - "ctrl-1": [ - "pane::ActivateItem", - 0 - ], - "ctrl-2": [ - "pane::ActivateItem", - 1 - ], - "ctrl-3": [ - "pane::ActivateItem", - 2 - ], - "ctrl-4": [ - "pane::ActivateItem", - 3 - ], - "ctrl-5": [ - "pane::ActivateItem", - 4 - ], - "ctrl-6": [ - "pane::ActivateItem", - 5 - ], - "ctrl-7": [ - "pane::ActivateItem", - 6 - ], - "ctrl-8": [ - "pane::ActivateItem", - 7 - ], - "ctrl-9": [ - "pane::ActivateItem", - 8 - ], + "ctrl-1": ["pane::ActivateItem", 0], + "ctrl-2": ["pane::ActivateItem", 1], + "ctrl-3": ["pane::ActivateItem", 2], + "ctrl-4": ["pane::ActivateItem", 3], + "ctrl-5": ["pane::ActivateItem", 4], + "ctrl-6": ["pane::ActivateItem", 5], + "ctrl-7": ["pane::ActivateItem", 6], + "ctrl-8": ["pane::ActivateItem", 7], + "ctrl-9": ["pane::ActivateItem", 8], "ctrl-0": "pane::ActivateLastItem", "ctrl--": "pane::GoBack", "ctrl-_": "pane::GoForward", @@ -416,42 +389,15 @@ { "context": "Workspace", "bindings": { - "cmd-1": [ - "workspace::ActivatePane", - 0 - ], - "cmd-2": [ - "workspace::ActivatePane", - 1 - ], - "cmd-3": [ - "workspace::ActivatePane", - 2 - ], - "cmd-4": [ - "workspace::ActivatePane", - 3 - ], - "cmd-5": [ - "workspace::ActivatePane", - 4 - ], - "cmd-6": [ - "workspace::ActivatePane", - 5 - ], - "cmd-7": [ - "workspace::ActivatePane", - 6 - ], - "cmd-8": [ - "workspace::ActivatePane", - 7 - ], - "cmd-9": [ - "workspace::ActivatePane", - 8 - ], + "cmd-1": ["workspace::ActivatePane", 0], + "cmd-2": ["workspace::ActivatePane", 1], + "cmd-3": ["workspace::ActivatePane", 2], + "cmd-4": ["workspace::ActivatePane", 3], + "cmd-5": ["workspace::ActivatePane", 4], + "cmd-6": ["workspace::ActivatePane", 5], + "cmd-7": ["workspace::ActivatePane", 6], + "cmd-8": ["workspace::ActivatePane", 7], + "cmd-9": ["workspace::ActivatePane", 8], "cmd-b": "workspace::ToggleLeftDock", "cmd-r": "workspace::ToggleRightDock", "cmd-j": "workspace::ToggleBottomDock", @@ -494,38 +440,14 @@ }, { "bindings": { - "cmd-k cmd-left": [ - "workspace::ActivatePaneInDirection", - "Left" - ], - "cmd-k cmd-right": [ - "workspace::ActivatePaneInDirection", - "Right" - ], - "cmd-k cmd-up": [ - "workspace::ActivatePaneInDirection", - "Up" - ], - "cmd-k cmd-down": [ - "workspace::ActivatePaneInDirection", - "Down" - ], - "cmd-k shift-left": [ - "workspace::SwapPaneInDirection", - "Left" - ], - "cmd-k shift-right": [ - "workspace::SwapPaneInDirection", - "Right" - ], - "cmd-k shift-up": [ - "workspace::SwapPaneInDirection", - "Up" - ], - "cmd-k shift-down": [ - "workspace::SwapPaneInDirection", - "Down" - ] + "cmd-k cmd-left": ["workspace::ActivatePaneInDirection", "Left"], + "cmd-k cmd-right": ["workspace::ActivatePaneInDirection", "Right"], + "cmd-k cmd-up": ["workspace::ActivatePaneInDirection", "Up"], + "cmd-k cmd-down": ["workspace::ActivatePaneInDirection", "Down"], + "cmd-k shift-left": ["workspace::SwapPaneInDirection", "Left"], + "cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"], + "cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"], + "cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"] } }, // Bindings from Atom @@ -627,14 +549,6 @@ "space": "collab_panel::InsertSpace" } }, - { - "context": "(CollabPanel && not_editing) > Editor", - "bindings": { - "cmd-c": "collab_panel::StartLinkChannel", - "cmd-x": "collab_panel::StartMoveChannel", - "cmd-v": "collab_panel::MoveOrLinkToSelected" - } - }, { "context": "ChannelModal", "bindings": { @@ -655,57 +569,21 @@ "cmd-v": "terminal::Paste", "cmd-k": "terminal::Clear", // Some nice conveniences - "cmd-backspace": [ - "terminal::SendText", - "\u0015" - ], - "cmd-right": [ - "terminal::SendText", - "\u0005" - ], - "cmd-left": [ - "terminal::SendText", - "\u0001" - ], + "cmd-backspace": ["terminal::SendText", "\u0015"], + "cmd-right": ["terminal::SendText", "\u0005"], + "cmd-left": ["terminal::SendText", "\u0001"], // Terminal.app compatibility - "alt-left": [ - "terminal::SendText", - "\u001bb" - ], - "alt-right": [ - "terminal::SendText", - "\u001bf" - ], + "alt-left": ["terminal::SendText", "\u001bb"], + "alt-right": ["terminal::SendText", "\u001bf"], // There are conflicting bindings for these keys in the global context. // these bindings override them, remove at your own risk: - "up": [ - "terminal::SendKeystroke", - "up" - ], - "pageup": [ - "terminal::SendKeystroke", - "pageup" - ], - "down": [ - "terminal::SendKeystroke", - "down" - ], - "pagedown": [ - "terminal::SendKeystroke", - "pagedown" - ], - "escape": [ - "terminal::SendKeystroke", - "escape" - ], - "enter": [ - "terminal::SendKeystroke", - "enter" - ], - "ctrl-c": [ - "terminal::SendKeystroke", - "ctrl-c" - ] + "up": ["terminal::SendKeystroke", "up"], + "pageup": ["terminal::SendKeystroke", "pageup"], + "down": ["terminal::SendKeystroke", "down"], + "pagedown": ["terminal::SendKeystroke", "pagedown"], + "escape": ["terminal::SendKeystroke", "escape"], + "enter": ["terminal::SendKeystroke", "enter"], + "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"] } } ] diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 4e52f57f60..86045e981a 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -55,7 +55,7 @@ pub enum Event { pub struct Room { id: u64, - channel_id: Option, + pub channel_id: Option, live_kit: Option, status: RoomStatus, shared_projects: HashSet>, diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index ab7ea78ac1..9089973d32 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,4 +1,4 @@ -use crate::Channel; +use crate::{Channel, ChannelId, ChannelStore}; use anyhow::Result; use client::{Client, Collaborator, UserStore}; use collections::HashMap; @@ -19,10 +19,11 @@ pub(crate) fn init(client: &Arc) { } pub struct ChannelBuffer { - pub(crate) channel: Arc, + pub channel_id: ChannelId, connected: bool, collaborators: HashMap, user_store: ModelHandle, + channel_store: ModelHandle, buffer: ModelHandle, buffer_epoch: u64, client: Arc, @@ -34,6 +35,7 @@ pub enum ChannelBufferEvent { CollaboratorsChanged, Disconnected, BufferEdited, + ChannelChanged, } impl Entity for ChannelBuffer { @@ -46,7 +48,7 @@ impl Entity for ChannelBuffer { } self.client .send(proto::LeaveChannelBuffer { - channel_id: self.channel.id, + channel_id: self.channel_id, }) .log_err(); } @@ -58,6 +60,7 @@ impl ChannelBuffer { channel: Arc, client: Arc, user_store: ModelHandle, + channel_store: ModelHandle, mut cx: AsyncAppContext, ) -> Result> { let response = client @@ -90,9 +93,10 @@ impl ChannelBuffer { connected: true, collaborators: Default::default(), acknowledge_task: None, - channel, + channel_id: channel.id, subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), user_store, + channel_store, }; this.replace_collaborators(response.collaborators, cx); this @@ -179,7 +183,7 @@ impl ChannelBuffer { let operation = language::proto::serialize_operation(operation); self.client .send(proto::UpdateChannelBuffer { - channel_id: self.channel.id, + channel_id: self.channel_id, operations: vec![operation], }) .log_err(); @@ -223,12 +227,15 @@ impl ChannelBuffer { &self.collaborators } - pub fn channel(&self) -> Arc { - self.channel.clone() + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_store + .read(cx) + .channel_for_id(self.channel_id) + .cloned() } pub(crate) fn disconnect(&mut self, cx: &mut ModelContext) { - log::info!("channel buffer {} disconnected", self.channel.id); + log::info!("channel buffer {} disconnected", self.channel_id); if self.connected { self.connected = false; self.subscription.take(); @@ -237,6 +244,11 @@ impl ChannelBuffer { } } + pub(crate) fn channel_changed(&mut self, cx: &mut ModelContext) { + cx.emit(ChannelBufferEvent::ChannelChanged); + cx.notify() + } + pub fn is_connected(&self) -> bool { self.connected } diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 734182886b..299c91759c 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -14,7 +14,7 @@ use time::OffsetDateTime; use util::{post_inc, ResultExt as _, TryFutureExt}; pub struct ChannelChat { - channel: Arc, + pub channel_id: ChannelId, messages: SumTree, channel_store: ModelHandle, loaded_all_messages: bool, @@ -74,7 +74,7 @@ impl Entity for ChannelChat { fn release(&mut self, _: &mut AppContext) { self.rpc .send(proto::LeaveChannelChat { - channel_id: self.channel.id, + channel_id: self.channel_id, }) .log_err(); } @@ -99,7 +99,7 @@ impl ChannelChat { Ok(cx.add_model(|cx| { let mut this = Self { - channel, + channel_id: channel.id, user_store, channel_store, rpc: client, @@ -116,8 +116,11 @@ impl ChannelChat { })) } - pub fn channel(&self) -> &Arc { - &self.channel + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_store + .read(cx) + .channel_for_id(self.channel_id) + .cloned() } pub fn send_message( @@ -135,7 +138,7 @@ impl ChannelChat { .current_user() .ok_or_else(|| anyhow!("current_user is not present"))?; - let channel_id = self.channel.id; + let channel_id = self.channel_id; let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id)); let nonce = self.rng.gen(); self.insert_messages( @@ -178,7 +181,7 @@ impl ChannelChat { pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext) -> Task> { let response = self.rpc.request(proto::RemoveChannelMessage { - channel_id: self.channel.id, + channel_id: self.channel_id, message_id: id, }); cx.spawn(|this, mut cx| async move { @@ -195,7 +198,7 @@ impl ChannelChat { if !self.loaded_all_messages { let rpc = self.rpc.clone(); let user_store = self.user_store.clone(); - let channel_id = self.channel.id; + let channel_id = self.channel_id; if let Some(before_message_id) = self.messages.first().and_then(|message| match message.id { ChannelMessageId::Saved(id) => Some(id), @@ -236,13 +239,13 @@ impl ChannelChat { { self.rpc .send(proto::AckChannelMessage { - channel_id: self.channel.id, + channel_id: self.channel_id, message_id: latest_message_id, }) .ok(); self.last_acknowledged_id = Some(latest_message_id); self.channel_store.update(cx, |store, cx| { - store.acknowledge_message_id(self.channel.id, latest_message_id, cx); + store.acknowledge_message_id(self.channel_id, latest_message_id, cx); }); } } @@ -251,7 +254,7 @@ impl ChannelChat { pub fn rejoin(&mut self, cx: &mut ModelContext) { let user_store = self.user_store.clone(); let rpc = self.rpc.clone(); - let channel_id = self.channel.id; + let channel_id = self.channel_id; cx.spawn(|this, mut cx| { async move { let response = rpc.request(proto::JoinChannelChat { channel_id }).await?; @@ -348,7 +351,7 @@ impl ChannelChat { this.update(&mut cx, |this, cx| { this.insert_messages(SumTree::from_item(message, &()), cx); cx.emit(ChannelChatEvent::NewMessage { - channel_id: this.channel.id, + channel_id: this.channel_id, message_id, }) }); diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 4891b5a18e..82cb0432d3 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -72,6 +72,10 @@ impl Channel { slug.trim_matches(|c| c == '-').to_string() } + + pub fn can_edit_notes(&self) -> bool { + self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin + } } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] @@ -265,10 +269,11 @@ impl ChannelStore { ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); + let channel_store = cx.handle(); self.open_channel_resource( channel_id, |this| &mut this.opened_buffers, - |channel, cx| ChannelBuffer::new(channel, client, user_store, cx), + |channel, cx| ChannelBuffer::new(channel, client, user_store, channel_store, cx), cx, ) } @@ -778,7 +783,7 @@ impl ChannelStore { let channel_buffer = buffer.read(cx); let buffer = channel_buffer.buffer().read(cx); buffer_versions.push(proto::ChannelBufferVersion { - channel_id: channel_buffer.channel().id, + channel_id: channel_buffer.channel_id, epoch: channel_buffer.epoch(), version: language::proto::serialize_version(&buffer.version()), }); @@ -805,13 +810,13 @@ impl ChannelStore { }; channel_buffer.update(cx, |channel_buffer, cx| { - let channel_id = channel_buffer.channel().id; + 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 channel_id = channel_buffer.channel_id; let remote_version = language::proto::deserialize_version(&remote_buffer.version); @@ -934,11 +939,27 @@ impl ChannelStore { if channels_changed { if !payload.delete_channels.is_empty() { - self.channel_index.delete_channels(&payload.delete_channels); - self.channel_participants - .retain(|channel_id, _| !payload.delete_channels.contains(channel_id)); + let mut channels_to_delete: Vec = Vec::new(); + let mut channels_to_rehome: Vec = Vec::new(); + for channel_id in payload.delete_channels { + if payload + .channels + .iter() + .any(|channel| channel.id == channel_id) + { + channels_to_rehome.push(channel_id) + } else { + channels_to_delete.push(channel_id) + } + } - for channel_id in &payload.delete_channels { + self.channel_index.delete_channels(&channels_to_delete); + self.channel_index + .delete_paths_through_channels(&channels_to_rehome); + self.channel_participants + .retain(|channel_id, _| !channels_to_delete.contains(channel_id)); + + for channel_id in &channels_to_delete { let channel_id = *channel_id; if payload .channels @@ -959,7 +980,16 @@ impl ChannelStore { let mut index = self.channel_index.bulk_insert(); for channel in payload.channels { - index.insert(channel) + let id = channel.id; + let channel_changed = index.insert(channel); + + if channel_changed { + if let Some(OpenedModelHandle::Open(buffer)) = self.opened_buffers.get(&id) { + if let Some(buffer) = buffer.upgrade(cx) { + buffer.update(cx, ChannelBuffer::channel_changed); + } + } + } } for unseen_buffer_change in payload.unseen_channel_buffer_changes { diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 54de15974e..e5dc75c8b9 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -24,12 +24,16 @@ impl ChannelIndex { /// Delete the given channels from this index. pub fn delete_channels(&mut self, channels: &[ChannelId]) { + dbg!("delete_channels", &channels); self.channels_by_id .retain(|channel_id, _| !channels.contains(channel_id)); - self.paths.retain(|path| { - path.iter() - .all(|channel_id| self.channels_by_id.contains_key(channel_id)) - }); + self.delete_paths_through_channels(channels) + } + + pub fn delete_paths_through_channels(&mut self, channels: &[ChannelId]) { + dbg!("rehome_channels", &channels); + self.paths + .retain(|path| !path.iter().any(|channel_id| channels.contains(channel_id))); } pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard { @@ -121,9 +125,15 @@ impl<'a> ChannelPathsInsertGuard<'a> { insert_new_message(&mut self.channels_by_id, channel_id, message_id) } - pub fn insert(&mut self, channel_proto: proto::Channel) { + pub fn insert(&mut self, channel_proto: proto::Channel) -> bool { + let mut ret = false; if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { let existing_channel = Arc::make_mut(existing_channel); + + ret = existing_channel.visibility != channel_proto.visibility() + || existing_channel.role != channel_proto.role() + || existing_channel.name != channel_proto.name; + existing_channel.visibility = channel_proto.visibility(); existing_channel.role = channel_proto.role(); existing_channel.name = channel_proto.name; @@ -141,6 +151,7 @@ impl<'a> ChannelPathsInsertGuard<'a> { ); self.insert_root(channel_proto.id); } + ret } pub fn insert_edge(&mut self, channel_id: ChannelId, parent_id: ChannelId) { diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index f2ab2a9d09..01174fe3be 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -412,7 +412,7 @@ async fn test_channel_buffer_disconnect( channel_buffer_a.update(cx_a, |buffer, _| { assert_eq!( - buffer.channel().as_ref(), + buffer.channel(cx).unwrap().as_ref(), &channel(channel_id, "the-channel", proto::ChannelRole::Admin) ); assert!(!buffer.is_connected()); @@ -437,7 +437,7 @@ async fn test_channel_buffer_disconnect( // Channel buffer observed the deletion channel_buffer_b.update(cx_b, |buffer, _| { assert_eq!( - buffer.channel().as_ref(), + buffer.channel(cx).unwrap().as_ref(), &channel(channel_id, "the-channel", proto::ChannelRole::Member) ); assert!(!buffer.is_connected()); diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index f4eca6b5ab..9d05c3017f 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -98,7 +98,8 @@ impl RandomizedTest for RandomChannelBufferTest { 30..=40 => { if let Some(buffer) = channel_buffers.iter().choose(rng) { - let channel_name = buffer.read_with(cx, |b, _| b.channel().name.clone()); + let channel_name = + buffer.read_with(cx, |b, _| b.channel(cx).unwrap().name.clone()); break ChannelBufferOperation::LeaveChannelNotes { channel_name }; } } @@ -106,7 +107,7 @@ impl RandomizedTest for RandomChannelBufferTest { _ => { if let Some(buffer) = channel_buffers.iter().choose(rng) { break buffer.read_with(cx, |b, _| { - let channel_name = b.channel().name.clone(); + let channel_name = b.channel(cx).unwrap().name.clone(); let edits = b .buffer() .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3)); @@ -153,7 +154,7 @@ impl RandomizedTest for RandomChannelBufferTest { 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 { + if buffer.read(cx).channel(cx).unwrap().name == channel_name { left_buffer = Ok(buffer.clone()); false } else { @@ -179,7 +180,9 @@ impl RandomizedTest for RandomChannelBufferTest { client .channel_buffers() .iter() - .find(|buffer| buffer.read(cx).channel().name == channel_name) + .find(|buffer| { + buffer.read(cx).channel(cx).unwrap().name == channel_name + }) .cloned() }) .ok_or_else(|| TestError::Inapplicable)?; @@ -250,7 +253,7 @@ impl RandomizedTest for RandomChannelBufferTest { if let Some(channel_buffer) = client .channel_buffers() .iter() - .find(|b| b.read(cx).channel().id == channel_id.to_proto()) + .find(|b| b.read(cx).channel_id == channel_id.to_proto()) { let channel_buffer = channel_buffer.read(cx); diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 98790778c9..aac4c924f8 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -58,6 +58,7 @@ postage.workspace = true serde.workspace = true serde_derive.workspace = true time.workspace = true +smallvec.workspace = true [dev-dependencies] call = { path = "../call", features = ["test-support"] } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index e62ee8ef4b..1bdcebd018 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -15,13 +15,14 @@ use gpui::{ ViewContext, ViewHandle, }; use project::Project; +use smallvec::SmallVec; use std::{ any::{Any, TypeId}, sync::Arc, }; use util::ResultExt; use workspace::{ - item::{FollowableItem, Item, ItemHandle}, + item::{FollowableItem, Item, ItemEvent, ItemHandle}, register_followable_item, searchable::SearchableItemHandle, ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId, @@ -140,6 +141,12 @@ impl ChannelView { editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( channel_buffer.clone(), ))); + editor.set_read_only( + !channel_buffer + .read(cx) + .channel(cx) + .is_some_and(|c| c.can_edit_notes()), + ); editor }); let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); @@ -157,8 +164,8 @@ impl ChannelView { } } - pub fn channel(&self, cx: &AppContext) -> Arc { - self.channel_buffer.read(cx).channel() + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_buffer.read(cx).channel(cx) } fn handle_channel_buffer_event( @@ -172,6 +179,13 @@ impl ChannelView { editor.set_read_only(true); cx.notify(); }), + ChannelBufferEvent::ChannelChanged => { + self.editor.update(cx, |editor, cx| { + editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes())); + cx.emit(editor::Event::TitleChanged); + cx.notify() + }); + } ChannelBufferEvent::BufferEdited => { if cx.is_self_focused() || self.editor.is_focused(cx) { self.acknowledge_buffer_version(cx); @@ -179,7 +193,7 @@ impl ChannelView { self.channel_store.update(cx, |store, cx| { let channel_buffer = self.channel_buffer.read(cx); store.notes_changed( - channel_buffer.channel().id, + channel_buffer.channel_id, channel_buffer.epoch(), &channel_buffer.buffer().read(cx).version(), cx, @@ -187,7 +201,7 @@ impl ChannelView { }); } } - _ => {} + ChannelBufferEvent::CollaboratorsChanged => {} } } @@ -195,7 +209,7 @@ impl ChannelView { self.channel_store.update(cx, |store, cx| { let channel_buffer = self.channel_buffer.read(cx); store.acknowledge_notes_version( - channel_buffer.channel().id, + channel_buffer.channel_id, channel_buffer.epoch(), &channel_buffer.buffer().read(cx).version(), cx, @@ -250,11 +264,17 @@ impl Item for ChannelView { style: &theme::Tab, cx: &gpui::AppContext, ) -> AnyElement { - let channel_name = &self.channel_buffer.read(cx).channel().name; - let label = if self.channel_buffer.read(cx).is_connected() { - format!("#{}", channel_name) + let label = if let Some(channel) = self.channel(cx) { + match ( + channel.can_edit_notes(), + self.channel_buffer.read(cx).is_connected(), + ) { + (true, true) => format!("#{}", channel.name), + (false, true) => format!("#{} (read-only)", channel.name), + (_, false) => format!("#{} (disconnected)", channel.name), + } } else { - format!("#{} (disconnected)", channel_name) + format!("channel notes (disconnected)") }; Label::new(label, style.label.to_owned()).into_any() } @@ -298,6 +318,10 @@ impl Item for ChannelView { fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { self.editor.read(cx).pixel_position_of_cursor(cx) } + + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + editor::Editor::to_item_events(event) + } } impl FollowableItem for ChannelView { @@ -313,7 +337,7 @@ impl FollowableItem for ChannelView { Some(proto::view::Variant::ChannelView( proto::view::ChannelView { - channel_id: channel_buffer.channel().id, + channel_id: channel_buffer.channel_id, editor: if let Some(proto::view::Variant::Editor(proto)) = self.editor.read(cx).to_state_proto(cx) { diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index f0a6c96ced..8f492e2e36 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -267,11 +267,15 @@ impl ChatPanel { fn set_active_chat(&mut self, chat: ModelHandle, cx: &mut ViewContext) { if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) { - let id = chat.read(cx).channel().id; + let id = chat.read(cx).channel_id; { let chat = chat.read(cx); self.message_list.reset(chat.message_count()); - let placeholder = format!("Message #{}", chat.channel().name); + let placeholder = if let Some(channel) = chat.channel(cx) { + format!("Message #{}", channel.name) + } else { + "Message Channel".to_string() + }; self.input_editor.update(cx, move |editor, cx| { editor.set_placeholder_text(placeholder, cx); }); @@ -360,7 +364,7 @@ impl ChatPanel { let is_admin = self .channel_store .read(cx) - .is_channel_admin(active_chat.channel().id); + .is_channel_admin(active_chat.channel_id); let last_message = active_chat.message(ix.saturating_sub(1)); let this_message = active_chat.message(ix); let is_continuation = last_message.id != this_message.id @@ -645,7 +649,7 @@ impl ChatPanel { cx: &mut ViewContext, ) -> Task> { if let Some((chat, _)) = &self.active_chat { - if chat.read(cx).channel().id == selected_channel_id { + if chat.read(cx).channel_id == selected_channel_id { return Task::ready(Ok(())); } } @@ -664,7 +668,7 @@ impl ChatPanel { fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext) { if let Some((chat, _)) = &self.active_chat { - let channel_id = chat.read(cx).channel().id; + let channel_id = chat.read(cx).channel_id; if let Some(workspace) = self.workspace.upgrade(cx) { ChannelView::open(channel_id, workspace, cx).detach(); } @@ -673,7 +677,7 @@ impl ChatPanel { fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext) { if let Some((chat, _)) = &self.active_chat { - let channel_id = chat.read(cx).channel().id; + let channel_id = chat.read(cx).channel_id; ActiveCall::global(cx) .update(cx, |call, cx| call.join_channel(channel_id, cx)) .detach_and_log_err(cx); From aa4b8d72463e2a6c497fac28f0673f4a4c7e1706 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 19 Oct 2023 14:41:20 -0600 Subject: [PATCH 08/40] Fix notifications for membership changes too --- crates/channel/src/channel_store.rs | 22 +- .../src/channel_store/channel_index.rs | 6 - crates/collab/src/db.rs | 13 + crates/collab/src/db/queries/channels.rs | 298 +++++++++++------- crates/collab/src/db/tests/channel_tests.rs | 2 +- crates/collab/src/rpc.rs | 174 +++++----- .../collab/src/tests/channel_buffer_tests.rs | 12 +- crates/collab/src/tests/channel_tests.rs | 170 ++++++++-- .../src/tests/random_channel_buffer_tests.rs | 4 +- crates/live_kit_client/src/test.rs | 10 + crates/live_kit_server/src/api.rs | 10 + crates/live_kit_server/src/token.rs | 9 + 12 files changed, 470 insertions(+), 260 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 82cb0432d3..2e1e92431e 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -939,27 +939,11 @@ impl ChannelStore { if channels_changed { if !payload.delete_channels.is_empty() { - let mut channels_to_delete: Vec = Vec::new(); - let mut channels_to_rehome: Vec = Vec::new(); - for channel_id in payload.delete_channels { - if payload - .channels - .iter() - .any(|channel| channel.id == channel_id) - { - channels_to_rehome.push(channel_id) - } else { - channels_to_delete.push(channel_id) - } - } - - self.channel_index.delete_channels(&channels_to_delete); - self.channel_index - .delete_paths_through_channels(&channels_to_rehome); + self.channel_index.delete_channels(&payload.delete_channels); self.channel_participants - .retain(|channel_id, _| !channels_to_delete.contains(channel_id)); + .retain(|channel_id, _| !&payload.delete_channels.contains(channel_id)); - for channel_id in &channels_to_delete { + for channel_id in &payload.delete_channels { let channel_id = *channel_id; if payload .channels diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index e5dc75c8b9..b221ce1b02 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -24,14 +24,8 @@ impl ChannelIndex { /// Delete the given channels from this index. pub fn delete_channels(&mut self, channels: &[ChannelId]) { - dbg!("delete_channels", &channels); self.channels_by_id .retain(|channel_id, _| !channels.contains(channel_id)); - self.delete_paths_through_channels(channels) - } - - pub fn delete_paths_through_channels(&mut self, channels: &[ChannelId]) { - dbg!("rehome_channels", &channels); self.paths .retain(|path| !path.iter().any(|channel_id| channels.contains(channel_id))); } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 4f49b7ca39..936b655200 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -453,6 +453,19 @@ pub struct SetChannelVisibilityResult { pub participants_to_remove: HashSet, } +#[derive(Debug)] +pub struct MembershipUpdated { + pub channel_id: ChannelId, + pub new_channels: ChannelsForUser, + pub removed_channels: Vec, +} + +#[derive(Debug)] +pub enum SetMemberRoleResult { + InviteUpdated(Channel), + MembershipUpdated(MembershipUpdated), +} + #[derive(FromQueryResult, Debug, PartialEq, Eq, Hash)] pub struct Channel { pub id: ChannelId, diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index f7b7f6085f..dbc701e4ae 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -128,9 +128,9 @@ impl Database { user_id: UserId, connection: ConnectionId, environment: &str, - ) -> Result<(JoinRoom, Option)> { + ) -> Result<(JoinRoom, Option, ChannelRole)> { self.transaction(move |tx| async move { - let mut joined_channel_id = None; + let mut accept_invite_result = None; let channel = channel::Entity::find() .filter(channel::Column::Id.eq(channel_id)) @@ -147,9 +147,7 @@ impl Database { .await? { // note, this may be a parent channel - joined_channel_id = Some(invitation.channel_id); role = Some(invitation.role); - channel_member::Entity::update(channel_member::ActiveModel { accepted: ActiveValue::Set(true), ..invitation.into_active_model() @@ -157,6 +155,11 @@ impl Database { .exec(&*tx) .await?; + accept_invite_result = Some( + self.calculate_membership_updated(channel_id, user_id, &*tx) + .await?, + ); + debug_assert!( self.channel_role_for_user(channel_id, user_id, &*tx) .await? @@ -167,6 +170,7 @@ impl Database { if role.is_none() && channel.as_ref().map(|c| c.visibility) == Some(ChannelVisibility::Public) { + role = Some(ChannelRole::Guest); let channel_id_to_join = self .public_path_to_channel(channel_id, &*tx) .await? @@ -174,9 +178,6 @@ impl Database { .cloned() .unwrap_or(channel_id); - role = Some(ChannelRole::Guest); - joined_channel_id = Some(channel_id_to_join); - channel_member::Entity::insert(channel_member::ActiveModel { id: ActiveValue::NotSet, channel_id: ActiveValue::Set(channel_id_to_join), @@ -187,6 +188,11 @@ impl Database { .exec(&*tx) .await?; + accept_invite_result = Some( + self.calculate_membership_updated(channel_id, user_id, &*tx) + .await?, + ); + debug_assert!( self.channel_role_for_user(channel_id, user_id, &*tx) .await? @@ -205,7 +211,7 @@ impl Database { self.join_channel_room_internal(channel_id, room_id, user_id, connection, &*tx) .await - .map(|jr| (jr, joined_channel_id)) + .map(|jr| (jr, accept_invite_result, role.unwrap())) }) .await } @@ -345,7 +351,7 @@ impl Database { invitee_id: UserId, admin_id: UserId, role: ChannelRole, - ) -> Result<()> { + ) -> Result { self.transaction(move |tx| async move { self.check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; @@ -360,7 +366,17 @@ impl Database { .insert(&*tx) .await?; - Ok(()) + let channel = channel::Entity::find_by_id(channel_id) + .one(&*tx) + .await? + .unwrap(); + + Ok(Channel { + id: channel.id, + visibility: channel.visibility, + name: channel.name, + role, + }) }) .await } @@ -429,10 +445,10 @@ impl Database { channel_id: ChannelId, user_id: UserId, accept: bool, - ) -> Result<()> { + ) -> Result> { self.transaction(move |tx| async move { - let rows_affected = if accept { - channel_member::Entity::update_many() + if accept { + let rows_affected = channel_member::Entity::update_many() .set(channel_member::ActiveModel { accepted: ActiveValue::Set(accept), ..Default::default() @@ -445,33 +461,96 @@ impl Database { ) .exec(&*tx) .await? - .rows_affected - } else { - channel_member::ActiveModel { - channel_id: ActiveValue::Unchanged(channel_id), - user_id: ActiveValue::Unchanged(user_id), - ..Default::default() + .rows_affected; + + if rows_affected == 0 { + Err(anyhow!("no such invitation"))?; } - .delete(&*tx) - .await? - .rows_affected - }; + + return Ok(Some( + self.calculate_membership_updated(channel_id, user_id, &*tx) + .await?, + )); + } + + let rows_affected = channel_member::ActiveModel { + channel_id: ActiveValue::Unchanged(channel_id), + user_id: ActiveValue::Unchanged(user_id), + ..Default::default() + } + .delete(&*tx) + .await? + .rows_affected; if rows_affected == 0 { Err(anyhow!("no such invitation"))?; } - Ok(()) + Ok(None) }) .await } + async fn calculate_membership_updated( + &self, + channel_id: ChannelId, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result { + let mut channel_to_refresh = channel_id; + let mut removed_channels: Vec = Vec::new(); + + // if the user was previously a guest of a parent public channel they may have seen this + // channel (or its descendants) in the tree already. + // Now they have new permissions, the graph of channels needs updating from that point. + if let Some(public_parent) = self.public_parent_channel_id(channel_id, &*tx).await? { + if self + .channel_role_for_user(public_parent, user_id, &*tx) + .await? + == Some(ChannelRole::Guest) + { + channel_to_refresh = public_parent; + } + } + + // remove all descendant channels from the user's tree + removed_channels.append( + &mut self + .get_channel_descendants(vec![channel_to_refresh], &*tx) + .await? + .into_iter() + .map(|edge| ChannelId::from_proto(edge.channel_id)) + .collect(), + ); + + let new_channels = self + .get_user_channels(user_id, Some(channel_to_refresh), &*tx) + .await?; + + // We only add the current channel to "moved" if the user has lost access, + // otherwise it would be made a root channel on the client. + if !new_channels + .channels + .channels + .iter() + .any(|c| c.id == channel_id) + { + removed_channels.push(channel_id); + } + + Ok(MembershipUpdated { + channel_id, + new_channels, + removed_channels, + }) + } + pub async fn remove_channel_member( &self, channel_id: ChannelId, member_id: UserId, admin_id: UserId, - ) -> Result<()> { + ) -> Result { self.transaction(|tx| async move { self.check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; @@ -489,7 +568,9 @@ impl Database { Err(anyhow!("no such member"))?; } - Ok(()) + Ok(self + .calculate_membership_updated(channel_id, member_id, &*tx) + .await?) }) .await } @@ -535,44 +616,7 @@ impl Database { self.transaction(|tx| async move { let tx = tx; - let channel_memberships = channel_member::Entity::find() - .filter( - channel_member::Column::UserId - .eq(user_id) - .and(channel_member::Column::Accepted.eq(true)), - ) - .all(&*tx) - .await?; - - self.get_user_channels(user_id, channel_memberships, &tx) - .await - }) - .await - } - - pub async fn get_channel_for_user( - &self, - channel_id: ChannelId, - user_id: UserId, - ) -> Result { - self.transaction(|tx| async move { - let tx = tx; - let role = self - .check_user_is_channel_participant(channel_id, user_id, &*tx) - .await?; - - self.get_user_channels( - user_id, - vec![channel_member::Model { - id: Default::default(), - channel_id, - user_id, - role, - accepted: true, - }], - &tx, - ) - .await + self.get_user_channels(user_id, None, &tx).await }) .await } @@ -580,19 +624,42 @@ impl Database { pub async fn get_user_channels( &self, user_id: UserId, - channel_memberships: Vec, + parent_channel_id: Option, tx: &DatabaseTransaction, ) -> Result { + // note: we could (maybe) win some efficiency here when parent_channel_id + // is set by getting just the role for that channel, then getting descendants + // with roles attached; but that's not as straightforward as it sounds + // because we need to calculate the path to the channel to make the query + // efficient, which currently requires an extra round trip to the database. + // Fix this later... + let channel_memberships = channel_member::Entity::find() + .filter( + channel_member::Column::UserId + .eq(user_id) + .and(channel_member::Column::Accepted.eq(true)), + ) + .all(&*tx) + .await?; + + dbg!((user_id, &channel_memberships)); + let mut edges = self .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) .await?; - let mut role_for_channel: HashMap = HashMap::default(); + dbg!((user_id, &edges)); + + let mut role_for_channel: HashMap = HashMap::default(); for membership in channel_memberships.iter() { - role_for_channel.insert(membership.channel_id, membership.role); + let included = + parent_channel_id.is_none() || membership.channel_id == parent_channel_id.unwrap(); + role_for_channel.insert(membership.channel_id, (membership.role, included)); } + dbg!((&role_for_channel, parent_channel_id)); + for ChannelEdge { parent_id, channel_id, @@ -601,14 +668,26 @@ impl Database { let parent_id = ChannelId::from_proto(*parent_id); let channel_id = ChannelId::from_proto(*channel_id); debug_assert!(role_for_channel.get(&parent_id).is_some()); - let parent_role = role_for_channel[&parent_id]; - if let Some(existing_role) = role_for_channel.get(&channel_id) { - if existing_role.should_override(parent_role) { - continue; - } + let (parent_role, parent_included) = role_for_channel[&parent_id]; + + if let Some((existing_role, included)) = role_for_channel.get(&channel_id) { + role_for_channel.insert( + channel_id, + (existing_role.max(parent_role), *included || parent_included), + ); + } else { + role_for_channel.insert( + channel_id, + ( + parent_role, + parent_included + || parent_channel_id.is_none() + || Some(channel_id) == parent_channel_id, + ), + ); } - role_for_channel.insert(channel_id, parent_role); } + dbg!((&role_for_channel, parent_channel_id)); let mut channels: Vec = Vec::new(); let mut channels_to_remove: HashSet = HashSet::default(); @@ -620,11 +699,13 @@ impl Database { while let Some(row) = rows.next().await { let channel = row?; - let role = role_for_channel[&channel.id]; + let (role, included) = role_for_channel[&channel.id]; - if role == ChannelRole::Banned + if !included + || role == ChannelRole::Banned || role == ChannelRole::Guest && channel.visibility != ChannelVisibility::Public { + dbg!("remove", channel.id); channels_to_remove.insert(channel.id.0 as u64); continue; } @@ -633,7 +714,7 @@ impl Database { id: channel.id, name: channel.name, visibility: channel.visibility, - role: role, + role, }); } drop(rows); @@ -740,18 +821,8 @@ impl Database { } results.push(( member.user_id, - self.get_user_channels( - member.user_id, - vec![channel_member::Model { - id: Default::default(), - channel_id: new_parent, - user_id: member.user_id, - role: member.role, - accepted: true, - }], - &*tx, - ) - .await?, + self.get_user_channels(member.user_id, Some(new_parent), &*tx) + .await?, )) } @@ -782,18 +853,8 @@ impl Database { }; results.push(( member.user_id, - self.get_user_channels( - member.user_id, - vec![channel_member::Model { - id: Default::default(), - channel_id: public_parent, - user_id: member.user_id, - role: member.role, - accepted: true, - }], - &*tx, - ) - .await?, + self.get_user_channels(member.user_id, Some(public_parent), &*tx) + .await?, )) } @@ -806,7 +867,7 @@ impl Database { admin_id: UserId, for_user: UserId, role: ChannelRole, - ) -> Result { + ) -> Result { self.transaction(|tx| async move { self.check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; @@ -828,7 +889,24 @@ impl Database { update.role = ActiveValue::Set(role); let updated = channel_member::Entity::update(update).exec(&*tx).await?; - Ok(updated) + if !updated.accepted { + let channel = channel::Entity::find_by_id(channel_id) + .one(&*tx) + .await? + .unwrap(); + + return Ok(SetMemberRoleResult::InviteUpdated(Channel { + id: channel.id, + visibility: channel.visibility, + name: channel.name, + role, + })); + } + + Ok(SetMemberRoleResult::MembershipUpdated( + self.calculate_membership_updated(channel_id, for_user, &*tx) + .await?, + )) }) .await } @@ -1396,16 +1474,7 @@ impl Database { .await?; } - let membership = channel_member::Entity::find() - .filter( - channel_member::Column::ChannelId - .eq(channel) - .and(channel_member::Column::UserId.eq(user)), - ) - .all(tx) - .await?; - - let mut channel_info = self.get_user_channels(user, membership, &*tx).await?; + let mut channel_info = self.get_user_channels(user, Some(channel), &*tx).await?; channel_info.channels.edges.push(ChannelEdge { channel_id: channel.to_proto(), @@ -1466,8 +1535,6 @@ impl Database { .await? == 0; - dbg!(is_stranded, &paths); - // Make sure that there is always at least one path to the channel if is_stranded { let root_paths: Vec<_> = paths @@ -1481,7 +1548,6 @@ impl Database { }) .collect(); - dbg!(is_stranded, &root_paths); channel_path::Entity::insert_many(root_paths) .exec(&*tx) .await?; @@ -1528,6 +1594,8 @@ impl Database { .into_iter() .collect(); + dbg!(&participants_to_update); + let mut moved_channels: HashSet = HashSet::default(); moved_channels.insert(channel_id); for edge in self.get_channel_descendants([channel_id], &*tx).await? { diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 1767a773ff..405839ba18 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -160,7 +160,7 @@ async fn test_joining_channels(db: &Arc) { let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap(); // can join a room with membership to its channel - let (joined_room, _) = db + let (joined_room, _, _) = db .join_channel( channel_1, user_1, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index f8648a2b14..3372fe1e35 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -3,9 +3,9 @@ mod connection_pool; use crate::{ auth, db::{ - self, BufferId, ChannelId, ChannelsForUser, CreateChannelResult, Database, MessageId, - MoveChannelResult, ProjectId, RenameChannelResult, RoomId, ServerId, - SetChannelVisibilityResult, User, UserId, + self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreateChannelResult, Database, + MembershipUpdated, MessageId, MoveChannelResult, ProjectId, RenameChannelResult, RoomId, + ServerId, SetChannelVisibilityResult, User, UserId, }, executor::Executor, AppState, Result, @@ -2266,23 +2266,20 @@ async fn invite_channel_member( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let invitee_id = UserId::from_proto(request.user_id); - db.invite_channel_member( - channel_id, - invitee_id, - session.user_id, - request.role().into(), - ) - .await?; + let channel = db + .invite_channel_member( + channel_id, + invitee_id, + session.user_id, + request.role().into(), + ) + .await?; - let channel = db.get_channel(channel_id, session.user_id).await?; + let update = proto::UpdateChannels { + channel_invitations: vec![channel.to_proto()], + ..Default::default() + }; - let mut update = proto::UpdateChannels::default(); - update.channel_invitations.push(proto::Channel { - id: channel.id.to_proto(), - visibility: channel.visibility.into(), - name: channel.name, - role: request.role().into(), - }); for connection_id in session .connection_pool() .await @@ -2304,19 +2301,13 @@ async fn remove_channel_member( let channel_id = ChannelId::from_proto(request.channel_id); let member_id = UserId::from_proto(request.user_id); - db.remove_channel_member(channel_id, member_id, session.user_id) + let membership_updated = db + .remove_channel_member(channel_id, member_id, session.user_id) .await?; - let mut update = proto::UpdateChannels::default(); - update.delete_channels.push(channel_id.to_proto()); + dbg!(&membership_updated); - for connection_id in session - .connection_pool() - .await - .user_connection_ids(member_id) - { - session.peer.send(connection_id, update.clone())?; - } + notify_membership_updated(membership_updated, member_id, &session).await?; response.send(proto::Ack {})?; Ok(()) @@ -2347,6 +2338,9 @@ async fn set_channel_visibility( } for user_id in participants_to_remove { let update = proto::UpdateChannels { + // for public participants we only need to remove the current channel + // (not descendants) + // because they can still see any public descendants delete_channels: vec![channel_id.to_proto()], ..Default::default() }; @@ -2367,7 +2361,7 @@ async fn set_channel_member_role( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let member_id = UserId::from_proto(request.user_id); - let channel_member = db + let result = db .set_channel_member_role( channel_id, session.user_id, @@ -2376,26 +2370,24 @@ async fn set_channel_member_role( ) .await?; - let mut update = proto::UpdateChannels::default(); - if channel_member.accepted { - let channels = db.get_channel_for_user(channel_id, member_id).await?; - update = build_channels_update(channels, vec![]); - } else { - let channel = db.get_channel(channel_id, session.user_id).await?; - update.channel_invitations.push(proto::Channel { - id: channel_id.to_proto(), - visibility: channel.visibility.into(), - name: channel.name, - role: request.role().into(), - }); - } + match result { + db::SetMemberRoleResult::MembershipUpdated(membership_update) => { + notify_membership_updated(membership_update, member_id, &session).await?; + } + db::SetMemberRoleResult::InviteUpdated(channel) => { + let update = proto::UpdateChannels { + channel_invitations: vec![channel.to_proto()], + ..Default::default() + }; - for connection_id in session - .connection_pool() - .await - .user_connection_ids(member_id) - { - session.peer.send(connection_id, update.clone())?; + for connection_id in session + .connection_pool() + .await + .user_connection_ids(member_id) + { + session.peer.send(connection_id, update.clone())?; + } + } } response.send(proto::Ack {})?; @@ -2541,38 +2533,29 @@ async fn respond_to_channel_invite( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - db.respond_to_channel_invite(channel_id, session.user_id, request.accept) + let result = db + .respond_to_channel_invite(channel_id, session.user_id, request.accept) .await?; - if request.accept { - channel_membership_updated(db, channel_id, &session).await?; + if let Some(accept_invite_result) = result { + notify_membership_updated(accept_invite_result, session.user_id, &session).await?; } else { - let mut update = proto::UpdateChannels::default(); - update - .remove_channel_invitations - .push(channel_id.to_proto()); - session.peer.send(session.connection_id, update)?; - } + let update = proto::UpdateChannels { + remove_channel_invitations: vec![channel_id.to_proto()], + ..Default::default() + }; + + let connection_pool = session.connection_pool().await; + for connection_id in connection_pool.user_connection_ids(session.user_id) { + session.peer.send(connection_id, update.clone())?; + } + }; + response.send(proto::Ack {})?; Ok(()) } -async fn channel_membership_updated( - db: tokio::sync::MutexGuard<'_, DbHandle>, - channel_id: ChannelId, - session: &Session, -) -> Result<(), crate::Error> { - let result = db.get_channel_for_user(channel_id, session.user_id).await?; - let mut update = build_channels_update(result, vec![]); - update - .remove_channel_invitations - .push(channel_id.to_proto()); - - session.peer.send(session.connection_id, update)?; - Ok(()) -} - async fn join_channel( request: proto::JoinChannel, response: Response, @@ -2605,7 +2588,7 @@ async fn join_channel_internal( leave_room_for_session(&session).await?; let db = session.db().await; - let (joined_room, joined_channel) = db + let (joined_room, accept_invite_result, role) = db .join_channel( channel_id, session.user_id, @@ -2615,12 +2598,21 @@ async fn join_channel_internal( .await?; let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| { - let token = live_kit - .room_token( - &joined_room.room.live_kit_room, - &session.user_id.to_string(), - ) - .trace_err()?; + let token = if role == ChannelRole::Guest { + live_kit + .guest_token( + &joined_room.room.live_kit_room, + &session.user_id.to_string(), + ) + .trace_err()? + } else { + live_kit + .room_token( + &joined_room.room.live_kit_room, + &session.user_id.to_string(), + ) + .trace_err()? + }; Some(LiveKitConnectionInfo { server_url: live_kit.url().into(), @@ -2634,8 +2626,8 @@ async fn join_channel_internal( live_kit_connection_info, })?; - if let Some(joined_channel) = joined_channel { - channel_membership_updated(db, joined_channel, &session).await? + if let Some(accept_invite_result) = accept_invite_result { + notify_membership_updated(accept_invite_result, session.user_id, &session).await?; } room_updated(&joined_room.room, &session.peer); @@ -3051,6 +3043,26 @@ fn to_tungstenite_message(message: AxumMessage) -> TungsteniteMessage { } } +async fn notify_membership_updated( + result: MembershipUpdated, + user_id: UserId, + session: &Session, +) -> Result<()> { + let mut update = build_channels_update(result.new_channels, vec![]); + update.delete_channels = result + .removed_channels + .into_iter() + .map(|id| id.to_proto()) + .collect(); + update.remove_channel_invitations = vec![result.channel_id.to_proto()]; + + let connection_pool = session.connection_pool().await; + for connection_id in connection_pool.user_connection_ids(user_id) { + session.peer.send(connection_id, update.clone())?; + } + Ok(()) +} + fn build_channels_update( channels: ChannelsForUser, channel_invites: Vec, diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 01174fe3be..cb51c7c5f3 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -410,7 +410,7 @@ async fn test_channel_buffer_disconnect( server.disconnect_client(client_a.peer_id().unwrap()); deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - channel_buffer_a.update(cx_a, |buffer, _| { + channel_buffer_a.update(cx_a, |buffer, cx| { assert_eq!( buffer.channel(cx).unwrap().as_ref(), &channel(channel_id, "the-channel", proto::ChannelRole::Admin) @@ -435,7 +435,7 @@ async fn test_channel_buffer_disconnect( deterministic.run_until_parked(); // Channel buffer observed the deletion - channel_buffer_b.update(cx_b, |buffer, _| { + channel_buffer_b.update(cx_b, |buffer, cx| { assert_eq!( buffer.channel(cx).unwrap().as_ref(), &channel(channel_id, "the-channel", proto::ChannelRole::Member) @@ -699,7 +699,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( .await .unwrap(); channel_view_1_a.update(cx_a, |notes, cx| { - assert_eq!(notes.channel(cx).name, "channel-1"); + assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); notes.editor.update(cx, |editor, cx| { editor.insert("Hello from A.", cx); editor.change_selections(None, cx, |selections| { @@ -731,7 +731,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( .expect("active item is not a channel view") }); channel_view_1_b.read_with(cx_b, |notes, cx| { - assert_eq!(notes.channel(cx).name, "channel-1"); + assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); let editor = notes.editor.read(cx); assert_eq!(editor.text(cx), "Hello from A."); assert_eq!(editor.selections.ranges::(cx), &[3..4]); @@ -743,7 +743,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( .await .unwrap(); channel_view_2_a.read_with(cx_a, |notes, cx| { - assert_eq!(notes.channel(cx).name, "channel-2"); + assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); }); // Client B is taken to the notes for channel 2. @@ -760,7 +760,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( .expect("active item is not a channel view") }); channel_view_2_b.read_with(cx_b, |notes, cx| { - assert_eq!(notes.channel(cx).name, "channel-2"); + assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); }); } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index b9425cc629..3df9597777 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -48,13 +48,13 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), depth: 0, - user_is_admin: true, + role: ChannelRole::Admin, }, ExpectedChannel { id: channel_b_id, name: "channel-b".to_string(), depth: 1, - user_is_admin: true, + role: ChannelRole::Admin, }, ], ); @@ -95,7 +95,7 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), depth: 0, - user_is_admin: false, + role: ChannelRole::Member, }], ); @@ -142,13 +142,13 @@ async fn test_core_channels( ExpectedChannel { id: channel_a_id, name: "channel-a".to_string(), - user_is_admin: false, + role: ChannelRole::Member, depth: 0, }, ExpectedChannel { id: channel_b_id, name: "channel-b".to_string(), - user_is_admin: false, + role: ChannelRole::Member, depth: 1, }, ], @@ -171,19 +171,19 @@ async fn test_core_channels( ExpectedChannel { id: channel_a_id, name: "channel-a".to_string(), - user_is_admin: false, + role: ChannelRole::Member, depth: 0, }, ExpectedChannel { id: channel_b_id, name: "channel-b".to_string(), - user_is_admin: false, + role: ChannelRole::Member, depth: 1, }, ExpectedChannel { id: channel_c_id, name: "channel-c".to_string(), - user_is_admin: false, + role: ChannelRole::Member, depth: 2, }, ], @@ -215,19 +215,19 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), depth: 0, - user_is_admin: true, + role: ChannelRole::Admin, }, ExpectedChannel { id: channel_b_id, name: "channel-b".to_string(), depth: 1, - user_is_admin: true, + role: ChannelRole::Admin, }, ExpectedChannel { id: channel_c_id, name: "channel-c".to_string(), depth: 2, - user_is_admin: true, + role: ChannelRole::Admin, }, ], ); @@ -249,7 +249,7 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), depth: 0, - user_is_admin: true, + role: ChannelRole::Admin, }], ); assert_channels( @@ -259,7 +259,7 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), depth: 0, - user_is_admin: true, + role: ChannelRole::Admin, }], ); @@ -282,7 +282,7 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), depth: 0, - user_is_admin: true, + role: ChannelRole::Admin, }], ); @@ -304,7 +304,7 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), depth: 0, - user_is_admin: true, + role: ChannelRole::Admin, }], ); } @@ -412,7 +412,7 @@ async fn test_channel_room( id: zed_id, name: "zed".to_string(), depth: 0, - user_is_admin: false, + role: ChannelRole::Member, }], ); client_b.channel_store().read_with(cx_b, |channels, _| { @@ -645,7 +645,7 @@ async fn test_permissions_update_while_invited( depth: 0, id: rust_id, name: "rust".to_string(), - user_is_admin: false, + role: ChannelRole::Member, }], ); assert_channels(client_b.channel_store(), cx_b, &[]); @@ -673,7 +673,7 @@ async fn test_permissions_update_while_invited( depth: 0, id: rust_id, name: "rust".to_string(), - user_is_admin: false, + role: ChannelRole::Member, }], ); assert_channels(client_b.channel_store(), cx_b, &[]); @@ -713,7 +713,7 @@ async fn test_channel_rename( depth: 0, id: rust_id, name: "rust-archive".to_string(), - user_is_admin: true, + role: ChannelRole::Admin, }], ); @@ -725,7 +725,7 @@ async fn test_channel_rename( depth: 0, id: rust_id, name: "rust-archive".to_string(), - user_is_admin: false, + role: ChannelRole::Member, }], ); } @@ -848,7 +848,7 @@ async fn test_lost_channel_creation( depth: 0, id: channel_id, name: "x".to_string(), - user_is_admin: false, + role: ChannelRole::Member, }], ); @@ -872,13 +872,13 @@ async fn test_lost_channel_creation( depth: 0, id: channel_id, name: "x".to_string(), - user_is_admin: true, + role: ChannelRole::Admin, }, ExpectedChannel { depth: 1, id: subchannel_id, name: "subchannel".to_string(), - user_is_admin: true, + role: ChannelRole::Admin, }, ], ); @@ -903,13 +903,13 @@ async fn test_lost_channel_creation( depth: 0, id: channel_id, name: "x".to_string(), - user_is_admin: false, + role: ChannelRole::Member, }, ExpectedChannel { depth: 1, id: subchannel_id, name: "subchannel".to_string(), - user_is_admin: false, + role: ChannelRole::Member, }, ], ); @@ -969,8 +969,7 @@ async fn test_channel_link_notifications( // we have an admin (a), member (b) and guest (c) all part of the zed channel. - // create a new private sub-channel - // create a new priate channel, make it public, and move it under the previous one, and verify it shows for b and c + // create a new private channel, make it public, and move it under the previous one, and verify it shows for b and not c let active_channel = client_a .channel_store() .update(cx_a, |channel_store, cx| { @@ -1118,6 +1117,117 @@ async fn test_channel_link_notifications( ); } +#[gpui::test] +async fn test_channel_membership_notifications( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + + 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_c").await; + + let user_b = client_b.user_id().unwrap(); + + let channels = server + .make_channel_tree( + &[ + ("zed", None), + ("active", Some("zed")), + ("vim", Some("active")), + ], + (&client_a, cx_a), + ) + .await; + let zed_channel = channels[0]; + let _active_channel = channels[1]; + let vim_channel = channels[2]; + + try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| { + [ + channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx), + channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx), + channel_store.invite_member(vim_channel, user_b, proto::ChannelRole::Member, cx), + channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Guest, cx), + ] + })) + .await + .unwrap(); + + deterministic.run_until_parked(); + + client_b + .channel_store() + .update(cx_b, |channel_store, _| { + channel_store.respond_to_channel_invite(zed_channel, true) + }) + .await + .unwrap(); + + client_b + .channel_store() + .update(cx_b, |channel_store, _| { + channel_store.respond_to_channel_invite(vim_channel, true) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + // we have an admin (a), and a guest (b) with access to all of zed, and membership in vim. + assert_channels( + client_b.channel_store(), + cx_b, + &[ + ExpectedChannel { + depth: 0, + id: zed_channel, + name: "zed".to_string(), + role: ChannelRole::Guest, + }, + ExpectedChannel { + depth: 1, + id: vim_channel, + name: "vim".to_string(), + role: ChannelRole::Member, + }, + ], + ); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.remove_member(vim_channel, user_b, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + assert_channels( + client_b.channel_store(), + cx_b, + &[ + ExpectedChannel { + depth: 0, + id: zed_channel, + name: "zed".to_string(), + role: ChannelRole::Guest, + }, + ExpectedChannel { + depth: 1, + id: vim_channel, + name: "vim".to_string(), + role: ChannelRole::Guest, + }, + ], + ) +} + #[gpui::test] async fn test_guest_access( deterministic: Arc, @@ -1485,7 +1595,7 @@ struct ExpectedChannel { depth: usize, id: ChannelId, name: String, - user_is_admin: bool, + role: ChannelRole, } #[track_caller] @@ -1502,7 +1612,7 @@ fn assert_channel_invitations( depth: 0, name: channel.name.clone(), id: channel.id, - user_is_admin: store.is_channel_admin(channel.id), + role: channel.role, }) .collect::>() }); @@ -1522,7 +1632,7 @@ fn assert_channels( depth, name: channel.name.clone(), id: channel.id, - user_is_admin: store.is_channel_admin(channel.id), + role: channel.role, }) .collect::>() }); diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index 9d05c3017f..64fecd5e62 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -99,14 +99,14 @@ impl RandomizedTest for RandomChannelBufferTest { 30..=40 => { if let Some(buffer) = channel_buffers.iter().choose(rng) { let channel_name = - buffer.read_with(cx, |b, _| b.channel(cx).unwrap().name.clone()); + buffer.read_with(cx, |b, cx| b.channel(cx).unwrap().name.clone()); break ChannelBufferOperation::LeaveChannelNotes { channel_name }; } } _ => { if let Some(buffer) = channel_buffers.iter().choose(rng) { - break buffer.read_with(cx, |b, _| { + break buffer.read_with(cx, |b, cx| { let channel_name = b.channel(cx).unwrap().name.clone(); let edits = b .buffer() diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 8df8ab4abb..b39ad333d2 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -306,6 +306,16 @@ impl live_kit_server::api::Client for TestApiClient { token::VideoGrant::to_join(room), ) } + + fn guest_token(&self, room: &str, identity: &str) -> Result { + let server = TestServer::get(&self.url)?; + token::create( + &server.api_key, + &server.secret_key, + Some(identity), + token::VideoGrant::for_guest(room), + ) + } } pub type Sid = String; diff --git a/crates/live_kit_server/src/api.rs b/crates/live_kit_server/src/api.rs index 417a17bdc9..2c1e174fb4 100644 --- a/crates/live_kit_server/src/api.rs +++ b/crates/live_kit_server/src/api.rs @@ -12,6 +12,7 @@ pub trait Client: Send + Sync { async fn delete_room(&self, name: String) -> Result<()>; async fn remove_participant(&self, room: String, identity: String) -> Result<()>; fn room_token(&self, room: &str, identity: &str) -> Result; + fn guest_token(&self, room: &str, identity: &str) -> Result; } #[derive(Clone)] @@ -138,4 +139,13 @@ impl Client for LiveKitClient { token::VideoGrant::to_join(room), ) } + + fn guest_token(&self, room: &str, identity: &str) -> Result { + token::create( + &self.key, + &self.secret, + Some(identity), + token::VideoGrant::for_guest(room), + ) + } } diff --git a/crates/live_kit_server/src/token.rs b/crates/live_kit_server/src/token.rs index 072a8be0c9..b98f5892ae 100644 --- a/crates/live_kit_server/src/token.rs +++ b/crates/live_kit_server/src/token.rs @@ -57,6 +57,15 @@ impl<'a> VideoGrant<'a> { ..Default::default() } } + + pub fn for_guest(room: &'a str) -> Self { + Self { + room: Some(Cow::Borrowed(room)), + room_join: Some(true), + can_subscribe: Some(true), + ..Default::default() + } + } } pub fn create( From e03e5364d22d41cf356d633835e2983ebb349700 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 19 Oct 2023 23:23:33 -0600 Subject: [PATCH 09/40] Wire through LiveKit permissions --- crates/call/src/room.rs | 26 ++++++++++----- crates/collab/src/rpc.rs | 35 ++++++++++++-------- crates/collab_ui/src/collab_titlebar_item.rs | 15 ++++++--- crates/rpc/proto/zed.proto | 1 + 4 files changed, 51 insertions(+), 26 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 86045e981a..594d1b7892 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -122,6 +122,10 @@ impl Room { } } + pub fn can_publish(&self) -> bool { + self.live_kit.as_ref().is_some_and(|room| room.can_publish) + } + fn new( id: u64, channel_id: Option, @@ -181,20 +185,23 @@ impl Room { }); let connect = room.connect(&connection_info.server_url, &connection_info.token); - cx.spawn(|this, mut cx| async move { - connect.await?; + if connection_info.can_publish { + cx.spawn(|this, mut cx| async move { + connect.await?; - if !cx.read(Self::mute_on_join) { - this.update(&mut cx, |this, cx| this.share_microphone(cx)) - .await?; - } + if !cx.read(Self::mute_on_join) { + this.update(&mut cx, |this, cx| this.share_microphone(cx)) + .await?; + } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } Some(LiveKitRoom { room, + can_publish: connection_info.can_publish, screen_track: LocalTrack::None, microphone_track: LocalTrack::None, next_publish_id: 0, @@ -1498,6 +1505,7 @@ struct LiveKitRoom { deafened: bool, speaking: bool, next_publish_id: usize, + can_publish: bool, _maintain_room: Task<()>, _maintain_tracks: [Task<()>; 2], } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 3372fe1e35..9974ded821 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -948,6 +948,7 @@ async fn create_room( Some(proto::LiveKitConnectionInfo { server_url: live_kit.url().into(), token, + can_publish: true, }) }) } @@ -1028,6 +1029,7 @@ async fn join_room( Some(proto::LiveKitConnectionInfo { server_url: live_kit.url().into(), token, + can_publish: true, }) } else { None @@ -2598,25 +2600,32 @@ async fn join_channel_internal( .await?; let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| { - let token = if role == ChannelRole::Guest { - live_kit - .guest_token( - &joined_room.room.live_kit_room, - &session.user_id.to_string(), - ) - .trace_err()? + let (can_publish, token) = if role == ChannelRole::Guest { + ( + false, + live_kit + .guest_token( + &joined_room.room.live_kit_room, + &session.user_id.to_string(), + ) + .trace_err()?, + ) } else { - live_kit - .room_token( - &joined_room.room.live_kit_room, - &session.user_id.to_string(), - ) - .trace_err()? + ( + true, + live_kit + .room_token( + &joined_room.room.live_kit_room, + &session.user_id.to_string(), + ) + .trace_err()?, + ) }; Some(LiveKitConnectionInfo { server_url: live_kit.url().into(), token, + can_publish, }) }); diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 211ee863e8..1adc03d5ea 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -88,8 +88,10 @@ impl View for CollabTitlebarItem { .zip(peer_id) .zip(ActiveCall::global(cx).read(cx).room().cloned()) { - right_container - .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); + if room.read(cx).can_publish() { + right_container + .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); + } right_container.add_child(self.render_leave_call(&theme, cx)); let muted = room.read(cx).is_muted(cx); let speaking = room.read(cx).is_speaking(); @@ -97,9 +99,14 @@ impl View for CollabTitlebarItem { self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx), ); left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx)); - right_container.add_child(self.render_toggle_mute(&theme, &room, cx)); + if room.read(cx).can_publish() { + right_container.add_child(self.render_toggle_mute(&theme, &room, cx)); + } right_container.add_child(self.render_toggle_deafen(&theme, &room, cx)); - right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx)); + if room.read(cx).can_publish() { + right_container + .add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx)); + } } let status = workspace.read(cx).client().status(); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 20a47954a4..5d0068b43e 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -335,6 +335,7 @@ message RoomUpdated { message LiveKitConnectionInfo { string server_url = 1; string token = 2; + bool can_publish = 3; } message ShareProject { From fd8e6110b1328e8ee6d8828d28620c7b53574167 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 20 Oct 2023 14:31:10 -0600 Subject: [PATCH 10/40] Fix panic by disallowing multiple room joins --- crates/call/src/call.rs | 89 ++++++++++++++++++++++++++++--- crates/call/src/room.rs | 44 +++++++-------- crates/workspace/src/workspace.rs | 4 ++ 3 files changed, 104 insertions(+), 33 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 0846341325..98233e1a10 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -10,7 +10,7 @@ use client::{ ZED_ALWAYS_ACTIVE, }; use collections::HashSet; -use futures::{future::Shared, FutureExt}; +use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use gpui::{ AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task, WeakModelHandle, @@ -37,6 +37,31 @@ pub struct IncomingCall { pub initial_project: Option, } +pub struct OneAtATime { + cancel: Option>, +} + +impl OneAtATime { + /// spawn a task in the given context. + /// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None) + /// otherwise you'll see the result of the task. + fn spawn(&mut self, cx: &mut AppContext, f: F) -> Task>> + where + F: 'static + FnOnce(AsyncAppContext) -> Fut, + Fut: Future>, + R: 'static, + { + let (tx, rx) = oneshot::channel(); + self.cancel.replace(tx); + cx.spawn(|cx| async move { + futures::select_biased! { + _ = rx.fuse() => Ok(None), + result = f(cx).fuse() => result.map(Some), + } + }) + } +} + /// Singleton global maintaining the user's participation in a room across workspaces. pub struct ActiveCall { room: Option<(ModelHandle, Vec)>, @@ -49,6 +74,7 @@ pub struct ActiveCall { ), client: Arc, user_store: ModelHandle, + _join_debouncer: OneAtATime, _subscriptions: Vec, } @@ -69,6 +95,7 @@ impl ActiveCall { pending_invites: Default::default(), incoming_call: watch::channel(), + _join_debouncer: OneAtATime { cancel: None }, _subscriptions: vec![ client.add_request_handler(cx.handle(), Self::handle_incoming_call), client.add_message_handler(cx.handle(), Self::handle_call_canceled), @@ -259,11 +286,16 @@ impl ActiveCall { return Task::ready(Err(anyhow!("no incoming call"))); }; - let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx); + let room_id = call.room_id.clone(); + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let join = self + ._join_debouncer + .spawn(cx, move |cx| Room::join(room_id, client, user_store, cx)); cx.spawn(|this, mut cx| async move { let room = join.await?; - this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) + this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx)) .await?; this.update(&mut cx, |this, cx| { this.report_call_event("accept incoming", cx) @@ -290,20 +322,24 @@ impl ActiveCall { &mut self, channel_id: u64, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task>>> { if let Some(room) = self.room().cloned() { if room.read(cx).channel_id() == Some(channel_id) { - return Task::ready(Ok(room)); + return Task::ready(Ok(Some(room))); } else { room.update(cx, |room, cx| room.clear_state(cx)); } } - let join = Room::join_channel(channel_id, self.client.clone(), self.user_store.clone(), cx); + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let join = self._join_debouncer.spawn(cx, move |cx| async move { + Room::join_channel(channel_id, client, user_store, cx).await + }); - cx.spawn(|this, mut cx| async move { + cx.spawn(move |this, mut cx| async move { let room = join.await?; - this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) + this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx)) .await?; this.update(&mut cx, |this, cx| { this.report_call_event("join channel", cx) @@ -457,3 +493,40 @@ pub fn report_call_event_for_channel( }; telemetry.report_clickhouse_event(event, telemetry_settings); } + +#[cfg(test)] +mod test { + use gpui::TestAppContext; + + use crate::OneAtATime; + + #[gpui::test] + async fn test_one_at_a_time(cx: &mut TestAppContext) { + let mut one_at_a_time = OneAtATime { cancel: None }; + + assert_eq!( + cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) })) + .await + .unwrap(), + Some(1) + ); + + let (a, b) = cx.update(|cx| { + ( + one_at_a_time.spawn(cx, |_| async { + assert!(false); + Ok(2) + }), + one_at_a_time.spawn(cx, |_| async { Ok(3) }), + ) + }); + + assert_eq!(a.await.unwrap(), None); + assert_eq!(b.await.unwrap(), Some(3)); + + let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) })); + drop(one_at_a_time); + + assert_eq!(promise.await.unwrap(), None); + } +} diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 4e52f57f60..4cd815e856 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1,7 +1,6 @@ use crate::{ call_settings::CallSettings, participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack}, - IncomingCall, }; use anyhow::{anyhow, Result}; use audio::{Audio, Sound}; @@ -284,37 +283,32 @@ impl Room { }) } - pub(crate) fn join_channel( + pub(crate) async fn join_channel( channel_id: u64, client: Arc, user_store: ModelHandle, - cx: &mut AppContext, - ) -> Task>> { - cx.spawn(|cx| async move { - Self::from_join_response( - client.request(proto::JoinChannel { channel_id }).await?, - client, - user_store, - cx, - ) - }) + cx: AsyncAppContext, + ) -> Result> { + Self::from_join_response( + client.request(proto::JoinChannel { channel_id }).await?, + client, + user_store, + cx, + ) } - pub(crate) fn join( - call: &IncomingCall, + pub(crate) async fn join( + room_id: u64, client: Arc, user_store: ModelHandle, - cx: &mut AppContext, - ) -> Task>> { - let id = call.room_id; - cx.spawn(|cx| async move { - Self::from_join_response( - client.request(proto::JoinRoom { id }).await?, - client, - user_store, - cx, - ) - }) + cx: AsyncAppContext, + ) -> Result> { + Self::from_join_response( + client.request(proto::JoinRoom { id: room_id }).await?, + client, + user_store, + cx, + ) } pub fn mute_on_join(cx: &AppContext) -> bool { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e879b981ef..f0b2c7b076 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4238,6 +4238,10 @@ async fn join_channel_internal( }) .await?; + let Some(room) = room else { + return anyhow::Ok(true); + }; + room.update(cx, |room, _| room.room_update_completed()) .await; From 5365fd2149dc7c0991e10d8c8f2c568b5ce67090 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 21 Oct 2023 03:05:57 -0700 Subject: [PATCH 11/40] WIP: Add test for panic, temporarily rollback synchronization changes --- crates/call/src/call.rs | 17 ++-- crates/collab/src/tests.rs | 4 + crates/collab/src/tests/integration_tests.rs | 90 +++++++++++++++++++- 3 files changed, 102 insertions(+), 9 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 98233e1a10..dbd61431dc 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -66,6 +66,7 @@ impl OneAtATime { pub struct ActiveCall { room: Option<(ModelHandle, Vec)>, pending_room_creation: Option, Arc>>>>, + _join_debouncer: OneAtATime, location: Option>, pending_invites: HashSet, incoming_call: ( @@ -74,7 +75,6 @@ pub struct ActiveCall { ), client: Arc, user_store: ModelHandle, - _join_debouncer: OneAtATime, _subscriptions: Vec, } @@ -289,13 +289,12 @@ impl ActiveCall { let room_id = call.room_id.clone(); let client = self.client.clone(); let user_store = self.user_store.clone(); - let join = self - ._join_debouncer - .spawn(cx, move |cx| Room::join(room_id, client, user_store, cx)); + let join = + cx.spawn(|this, cx| async move { Room::join(room_id, client, user_store, cx).await }); cx.spawn(|this, mut cx| async move { let room = join.await?; - this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx)) + this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) .await?; this.update(&mut cx, |this, cx| { this.report_call_event("accept incoming", cx) @@ -333,18 +332,20 @@ impl ActiveCall { let client = self.client.clone(); let user_store = self.user_store.clone(); - let join = self._join_debouncer.spawn(cx, move |cx| async move { + let join = cx.spawn(|this, cx| async move { Room::join_channel(channel_id, client, user_store, cx).await }); + // self._join_debouncer.spawn(cx, move |cx| async move {}); + cx.spawn(move |this, mut cx| async move { let room = join.await?; - this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx)) + this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) .await?; this.update(&mut cx, |this, cx| { this.report_call_event("join channel", cx) }); - Ok(room) + Ok(Some(room)) }) } diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index e78bbe3466..811cb910e6 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -39,3 +39,7 @@ fn room_participants(room: &ModelHandle, cx: &mut TestAppContext) -> RoomP RoomParticipants { remote, pending } }) } + +fn channel_id(room: &ModelHandle, cx: &mut TestAppContext) -> u64 { + cx.read(|cx| room.read(cx).channel_id().unwrap()) +} diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index d6d449fd47..3bc4f17f85 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -1,6 +1,6 @@ use crate::{ rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, - tests::{room_participants, RoomParticipants, TestClient, TestServer}, + tests::{channel_id, room_participants, RoomParticipants, TestClient, TestServer}, }; use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{User, RECEIVE_TIMEOUT}; @@ -469,6 +469,94 @@ async fn test_calling_multiple_users_simultaneously( ); } +#[gpui::test(iterations = 10)] +async fn test_joining_channels_and_calling_multiple_users_simultaneously( + deterministic: Arc, + 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; + server + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + + let channel_1 = server + .make_channel( + "channel1", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + let channel_2 = server + .make_channel( + "channel2", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + + // Simultaneously join channel 1 and then channel 2 + active_call_a + .update(cx_a, |call, cx| call.join_channel(channel_1, cx)) + .detach(); + let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx)); + + join_channel_2.await.unwrap(); + + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + deterministic.run_until_parked(); + + assert_eq!(channel_id(&room_a, cx_a), channel_2); + + todo!(); + + // Leave the room + active_call_a + .update(cx_a, |call, cx| { + let hang_up = call.hang_up(cx); + hang_up + }) + .await + .unwrap(); + + // Simultaneously join channel 1 and call user B and user C from client A. + let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx)); + + let b_invite = active_call_a.update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) + }); + let c_invite = active_call_a.update(cx_a, |call, cx| { + call.invite(client_c.user_id().unwrap(), None, cx) + }); + join_channel.await.unwrap(); + b_invite.await.unwrap(); + c_invite.await.unwrap(); + + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + deterministic.run_until_parked(); + + assert_eq!( + room_participants(&room_a, cx_a), + RoomParticipants { + remote: Default::default(), + pending: vec!["user_b".to_string(), "user_c".to_string()] + } + ); + + assert_eq!(channel_id(&room_a, cx_a), channel_1); +} + #[gpui::test(iterations = 10)] async fn test_room_uniqueness( deterministic: Arc, From 7e4de2ac166d6d662cf08161770d09fad33510ea Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 21 Oct 2023 03:08:25 -0700 Subject: [PATCH 12/40] Restore synchronization --- crates/call/src/call.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index dbd61431dc..0f34741c1c 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -289,12 +289,13 @@ impl ActiveCall { let room_id = call.room_id.clone(); let client = self.client.clone(); let user_store = self.user_store.clone(); - let join = - cx.spawn(|this, cx| async move { Room::join(room_id, client, user_store, cx).await }); + let join = self + ._join_debouncer + .spawn(cx, move |cx| Room::join(room_id, client, user_store, cx)); cx.spawn(|this, mut cx| async move { let room = join.await?; - this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) + this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx)) .await?; this.update(&mut cx, |this, cx| { this.report_call_event("accept incoming", cx) @@ -332,20 +333,18 @@ impl ActiveCall { let client = self.client.clone(); let user_store = self.user_store.clone(); - let join = cx.spawn(|this, cx| async move { + let join = self._join_debouncer.spawn(cx, move |cx| async move { Room::join_channel(channel_id, client, user_store, cx).await }); - // self._join_debouncer.spawn(cx, move |cx| async move {}); - cx.spawn(move |this, mut cx| async move { let room = join.await?; - this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) + this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx)) .await?; this.update(&mut cx, |this, cx| { this.report_call_event("join channel", cx) }); - Ok(Some(room)) + Ok(room) }) } From b8936e5fca16a9b310c2fb2f116f7657138ca322 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 21 Oct 2023 03:15:18 -0700 Subject: [PATCH 13/40] Finish room initialization concurrency test --- crates/collab/src/tests.rs | 4 +- crates/collab/src/tests/integration_tests.rs | 41 ++++++++++++++++++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 811cb910e6..6fcb2165c0 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -40,6 +40,6 @@ fn room_participants(room: &ModelHandle, cx: &mut TestAppContext) -> RoomP }) } -fn channel_id(room: &ModelHandle, cx: &mut TestAppContext) -> u64 { - cx.read(|cx| room.read(cx).channel_id().unwrap()) +fn channel_id(room: &ModelHandle, cx: &mut TestAppContext) -> Option { + cx.read(|cx| room.read(cx).channel_id()) } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 3bc4f17f85..a3546b516d 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -517,9 +517,43 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); deterministic.run_until_parked(); - assert_eq!(channel_id(&room_a, cx_a), channel_2); + assert_eq!(channel_id(&room_a, cx_a), Some(channel_2)); - todo!(); + // Leave the room + active_call_a + .update(cx_a, |call, cx| { + let hang_up = call.hang_up(cx); + hang_up + }) + .await + .unwrap(); + + // Initiating invites and then joining a channel should fail gracefully + let b_invite = active_call_a.update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) + }); + let c_invite = active_call_a.update(cx_a, |call, cx| { + call.invite(client_c.user_id().unwrap(), None, cx) + }); + + let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx)); + + b_invite.await.unwrap(); + c_invite.await.unwrap(); + assert!(join_channel.await.is_err()); + + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + deterministic.run_until_parked(); + + assert_eq!( + room_participants(&room_a, cx_a), + RoomParticipants { + remote: Default::default(), + pending: vec!["user_b".to_string(), "user_c".to_string()] + } + ); + + assert_eq!(channel_id(&room_a, cx_a), None); // Leave the room active_call_a @@ -539,6 +573,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( let c_invite = active_call_a.update(cx_a, |call, cx| { call.invite(client_c.user_id().unwrap(), None, cx) }); + join_channel.await.unwrap(); b_invite.await.unwrap(); c_invite.await.unwrap(); @@ -554,7 +589,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( } ); - assert_eq!(channel_id(&room_a, cx_a), channel_1); + assert_eq!(channel_id(&room_a, cx_a), Some(channel_1)); } #[gpui::test(iterations = 10)] From 9589f5573d4629667b8464f05571619b26800805 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 16 Oct 2023 22:20:52 -0600 Subject: [PATCH 14/40] Fix some bugs with vim objects - softwrap interaction - correct selection if cursor is on opening marker --- crates/editor/src/movement.rs | 24 + crates/vim/src/normal/delete.rs | 6 +- crates/vim/src/object.rs | 272 +++-- .../src/test/neovim_backed_test_context.rs | 25 +- crates/vim/src/test/neovim_connection.rs | 26 +- crates/vim/src/visual.rs | 20 +- ..._change_surrounding_character_objects.json | 1020 +++++++++++++++++ crates/vim/test_data/test_delete_e.json | 8 - .../test_data/test_delete_next_word_end.json | 12 + ..._delete_surrounding_character_objects.json | 1020 +++++++++++++++++ ...ltiline_surrounding_character_objects.json | 5 + ...gleline_surrounding_character_objects.json | 27 + 12 files changed, 2323 insertions(+), 142 deletions(-) create mode 100644 crates/vim/test_data/test_delete_next_word_end.json create mode 100644 crates/vim/test_data/test_singleline_surrounding_character_objects.json diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 580faf1050..fd77e0cafe 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -369,6 +369,30 @@ pub fn find_boundary( map.clip_point(offset.to_display_point(map), Bias::Right) } +pub fn chars_after( + map: &DisplaySnapshot, + mut offset: usize, +) -> impl Iterator)> + '_ { + map.buffer_snapshot.chars_at(offset).map(move |ch| { + let before = offset; + offset = offset + ch.len_utf8(); + (ch, before..offset) + }) +} + +pub fn chars_before( + map: &DisplaySnapshot, + mut offset: usize, +) -> impl Iterator)> + '_ { + map.buffer_snapshot + .reversed_chars_at(offset) + .map(move |ch| { + let after = offset; + offset = offset - ch.len_utf8(); + (ch, offset..after) + }) +} + pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { let raw_point = point.to_point(map); let scope = map.buffer_snapshot.language_scope_at(raw_point); diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index 77e0e47be5..b8105aeb8d 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -193,10 +193,10 @@ mod test { } #[gpui::test] - async fn test_delete_e(cx: &mut gpui::TestAppContext) { + async fn test_delete_next_word_end(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "e"]); - cx.assert("Teˇst Test").await; - cx.assert("Tˇest test").await; + // cx.assert("Teˇst Test").await; + // cx.assert("Tˇest test").await; cx.assert(indoc! {" Test teˇst test"}) diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 653d4ca7b6..e4a0659f3f 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -2,7 +2,7 @@ use std::ops::Range; use editor::{ char_kind, - display_map::DisplaySnapshot, + display_map::{DisplaySnapshot, ToDisplayPoint}, movement::{self, FindRange}, Bias, CharKind, DisplayPoint, }; @@ -427,103 +427,141 @@ fn surrounding_markers( relative_to: DisplayPoint, around: bool, search_across_lines: bool, - start_marker: char, - end_marker: char, + open_marker: char, + close_marker: char, ) -> Option> { - let mut matched_ends = 0; - let mut start = None; - for (char, mut point) in map.reverse_chars_at(relative_to) { - if char == start_marker { - if matched_ends > 0 { - matched_ends -= 1; - } else { - if around { - start = Some(point) - } else { - *point.column_mut() += char.len_utf8() as u32; - start = Some(point) + let point = relative_to.to_offset(map, Bias::Left); + + let mut matched_closes = 0; + let mut opening = None; + + if let Some((ch, range)) = movement::chars_after(map, point).next() { + if ch == open_marker { + if open_marker == close_marker { + let mut total = 0; + for (ch, _) in movement::chars_before(map, point) { + if ch == '\n' { + break; + } + if ch == open_marker { + total += 1; + } } - break; + if total % 2 == 0 { + opening = Some(range) + } + } else { + opening = Some(range) } - } else if char == end_marker { - matched_ends += 1; - } else if char == '\n' && !search_across_lines { - break; } } - let mut matched_starts = 0; - let mut end = None; - for (char, mut point) in map.chars_at(relative_to) { - if char == end_marker { - if start.is_none() { + if opening.is_none() { + for (ch, range) in movement::chars_before(map, point) { + if ch == '\n' && !search_across_lines { break; } - if matched_starts > 0 { - matched_starts -= 1; - } else { - if around { - *point.column_mut() += char.len_utf8() as u32; - end = Some(point); - } else { - end = Some(point); + if ch == open_marker { + if matched_closes == 0 { + opening = Some(range); + break; } - - break; + matched_closes -= 1; + } else if ch == close_marker { + matched_closes += 1 } } - - if char == start_marker { - if start.is_none() { - if around { - start = Some(point); - } else { - *point.column_mut() += char.len_utf8() as u32; - start = Some(point); - } - } else { - matched_starts += 1; - } - } - - if char == '\n' && !search_across_lines { - break; - } } - let (Some(mut start), Some(mut end)) = (start, end) else { + if opening.is_none() { + for (ch, range) in movement::chars_after(map, point) { + if ch == open_marker { + opening = Some(range); + break; + } else if ch == close_marker { + break; + } + } + } + + let Some(mut opening) = opening else { return None; }; - if !around { - // if a block starts with a newline, move the start to after the newline. - let mut was_newline = false; - for (char, point) in map.chars_at(start) { - if was_newline { - start = point; - } else if char == '\n' { - was_newline = true; - continue; - } + let mut matched_opens = 0; + let mut closing = None; + + for (ch, range) in movement::chars_after(map, opening.end) { + if ch == '\n' && !search_across_lines { break; } - // if a block ends with a newline, then whitespace, then the delimeter, - // move the end to after the newline. - let mut new_end = end; - for (char, point) in map.reverse_chars_at(end) { - if char == '\n' { - end = new_end; + + if ch == close_marker { + if matched_opens == 0 { + closing = Some(range); break; } - if !char.is_whitespace() { - break; - } - new_end = point + matched_opens -= 1; + } else if ch == open_marker { + matched_opens += 1; } } - Some(start..end) + let Some(mut closing) = closing else { + return None; + }; + + if around && !search_across_lines { + let mut found = false; + + for (ch, range) in movement::chars_after(map, closing.end) { + if ch.is_whitespace() && ch != '\n' { + found = true; + closing.end = range.end; + } else { + break; + } + } + + if !found { + for (ch, range) in movement::chars_before(map, opening.start) { + if ch.is_whitespace() && ch != '\n' { + opening.start = range.start + } else { + break; + } + } + } + } + + if !around && search_across_lines { + if let Some((ch, range)) = movement::chars_after(map, opening.end).next() { + if ch == '\n' { + opening.end = range.end + } + } + + for (ch, range) in movement::chars_before(map, closing.start) { + if !ch.is_whitespace() { + break; + } + if ch != '\n' { + closing.start = range.start + } + } + } + + let result = if around { + opening.start..closing.end + } else { + opening.end..closing.start + }; + + Some( + map.clip_point(result.start.to_display_point(map), Bias::Left) + ..map.clip_point(result.end.to_display_point(map), Bias::Right), + ) } #[cfg(test)] @@ -765,10 +803,7 @@ mod test { let mut cx = NeovimBackedTestContext::new(cx).await; for (start, end) in SURROUNDING_OBJECTS { - if ((start == &'\'' || start == &'`' || start == &'"') - && !ExemptionFeatures::QuotesSeekForward.supported()) - || (start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported()) - { + if start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported() { continue; } @@ -786,6 +821,63 @@ mod test { .await; } } + #[gpui::test] + async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_wrap(12).await; + + cx.set_shared_state(indoc! { + "helˇlo \"world\"!" + }) + .await; + cx.simulate_shared_keystrokes(["v", "i", "\""]).await; + cx.assert_shared_state(indoc! { + "hello \"«worldˇ»\"!" + }) + .await; + + cx.set_shared_state(indoc! { + "hello \"wˇorld\"!" + }) + .await; + cx.simulate_shared_keystrokes(["v", "i", "\""]).await; + cx.assert_shared_state(indoc! { + "hello \"«worldˇ»\"!" + }) + .await; + + cx.set_shared_state(indoc! { + "hello \"wˇorld\"!" + }) + .await; + cx.simulate_shared_keystrokes(["v", "a", "\""]).await; + cx.assert_shared_state(indoc! { + "hello« \"world\"ˇ»!" + }) + .await; + + cx.set_shared_state(indoc! { + "hello \"wˇorld\" !" + }) + .await; + cx.simulate_shared_keystrokes(["v", "a", "\""]).await; + cx.assert_shared_state(indoc! { + "hello «\"world\" ˇ»!" + }) + .await; + + cx.set_shared_state(indoc! { + "hello \"wˇorld\"• + goodbye" + }) + .await; + cx.simulate_shared_keystrokes(["v", "a", "\""]).await; + cx.assert_shared_state(indoc! { + "hello «\"world\" ˇ» + goodbye" + }) + .await; + } #[gpui::test] async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) { @@ -827,6 +919,25 @@ mod test { return false }"}) .await; + + cx.set_shared_state(indoc! { + "func empty(a string) bool { + if a == \"\" ˇ{ + return true + } + return false + }" + }) + .await; + cx.simulate_shared_keystrokes(["v", "i", "{"]).await; + cx.assert_shared_state(indoc! {" + func empty(a string) bool { + if a == \"\" { + « return true + ˇ» } + return false + }"}) + .await; } #[gpui::test] @@ -834,10 +945,7 @@ mod test { let mut cx = NeovimBackedTestContext::new(cx).await; for (start, end) in SURROUNDING_OBJECTS { - if ((start == &'\'' || start == &'`' || start == &'"') - && !ExemptionFeatures::QuotesSeekForward.supported()) - || (start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported()) - { + if start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported() { continue; } let marked_string = SURROUNDING_MARKER_STRING diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 324e2e9f45..a27676a8af 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -1,15 +1,12 @@ use editor::scroll::VERTICAL_SCROLL_MARGIN; use indoc::indoc; use settings::SettingsStore; -use std::ops::{Deref, DerefMut, Range}; +use std::ops::{Deref, DerefMut}; use collections::{HashMap, HashSet}; use gpui::{geometry::vector::vec2f, ContextHandle}; -use language::{ - language_settings::{AllLanguageSettings, SoftWrap}, - OffsetRangeExt, -}; -use util::test::{generate_marked_text, marked_text_offsets}; +use language::language_settings::{AllLanguageSettings, SoftWrap}; +use util::test::marked_text_offsets; use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext}; use crate::state::Mode; @@ -37,8 +34,6 @@ pub enum ExemptionFeatures { AroundSentenceStartingBetweenIncludesWrongWhitespace, // Non empty selection with text objects in visual mode NonEmptyVisualTextObjects, - // Quote style surrounding text objects don't seek forward properly - QuotesSeekForward, // Neovim freezes up for some reason with angle brackets AngleBracketsFreezeNeovim, // Sentence Doesn't backtrack when its at the end of the file @@ -250,25 +245,13 @@ impl<'a> NeovimBackedTestContext<'a> { } pub async fn neovim_state(&mut self) -> String { - generate_marked_text( - self.neovim.text().await.as_str(), - &self.neovim_selections().await[..], - true, - ) + self.neovim.marked_text().await } pub async fn neovim_mode(&mut self) -> Mode { self.neovim.mode().await.unwrap() } - async fn neovim_selections(&mut self) -> Vec> { - let neovim_selections = self.neovim.selections().await; - neovim_selections - .into_iter() - .map(|selection| selection.to_offset(&self.buffer_snapshot())) - .collect() - } - pub async fn assert_state_matches(&mut self) { self.is_dirty = false; let neovim = self.neovim_state().await; diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 38af2d1555..ddcab39cb2 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -1,9 +1,9 @@ +use std::path::PathBuf; #[cfg(feature = "neovim")] use std::{ cmp, - ops::{Deref, DerefMut}, + ops::{Deref, DerefMut, Range}, }; -use std::{ops::Range, path::PathBuf}; #[cfg(feature = "neovim")] use async_compat::Compat; @@ -12,6 +12,7 @@ use async_trait::async_trait; #[cfg(feature = "neovim")] use gpui::keymap_matcher::Keystroke; +#[cfg(feature = "neovim")] use language::Point; #[cfg(feature = "neovim")] @@ -296,7 +297,7 @@ impl NeovimConnection { } #[cfg(feature = "neovim")] - pub async fn state(&mut self) -> (Option, String, Vec>) { + pub async fn state(&mut self) -> (Option, String) { let nvim_buffer = self .nvim .get_current_buf() @@ -405,37 +406,33 @@ impl NeovimConnection { .push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)), } + let ranges = encode_ranges(&text, &selections); let state = NeovimData::Get { mode, - state: encode_ranges(&text, &selections), + state: ranges.clone(), }; if self.data.back() != Some(&state) { self.data.push_back(state.clone()); } - (mode, text, selections) + (mode, ranges) } #[cfg(not(feature = "neovim"))] - pub async fn state(&mut self) -> (Option, String, Vec>) { - if let Some(NeovimData::Get { state: text, mode }) = self.data.front() { - let (text, ranges) = parse_state(text); - (*mode, text, ranges) + pub async fn state(&mut self) -> (Option, String) { + if let Some(NeovimData::Get { state: raw, mode }) = self.data.front() { + (*mode, raw.to_string()) } else { panic!("operation does not match recorded script. re-record with --features=neovim"); } } - pub async fn selections(&mut self) -> Vec> { - self.state().await.2 - } - pub async fn mode(&mut self) -> Option { self.state().await.0 } - pub async fn text(&mut self) -> String { + pub async fn marked_text(&mut self) -> String { self.state().await.1 } @@ -527,6 +524,7 @@ impl Handler for NvimHandler { } } +#[cfg(feature = "neovim")] fn parse_state(marked_text: &str) -> (String, Vec>) { let (text, ranges) = util::test::marked_text_ranges(marked_text, true); let point_ranges = ranges diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 5d6477ff5b..ed244c75fd 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use std::{cmp, sync::Arc}; +use std::sync::Arc; use collections::HashMap; use editor::{ @@ -263,21 +263,13 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { if let Some(range) = object.range(map, head, around) { if !range.is_empty() { - let expand_both_ways = - if object.always_expands_both_ways() || selection.is_empty() { - true - // contains only one character - } else if let Some((_, start)) = - map.reverse_chars_at(selection.end).next() - { - selection.start == start - } else { - false - }; + let expand_both_ways = object.always_expands_both_ways() + || selection.is_empty() + || movement::right(map, selection.start) == selection.end; if expand_both_ways { - selection.start = cmp::min(selection.start, range.start); - selection.end = cmp::max(selection.end, range.end); + selection.start = range.start; + selection.end = range.end; } else if selection.reversed { selection.start = range.start; } else { diff --git a/crates/vim/test_data/test_change_surrounding_character_objects.json b/crates/vim/test_data/test_change_surrounding_character_objects.json index a88f84cf4b..a95fd8c56e 100644 --- a/crates/vim/test_data/test_change_surrounding_character_objects.json +++ b/crates/vim/test_data/test_change_surrounding_character_objects.json @@ -1,3 +1,1023 @@ +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'ˇ'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'ˇ'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''quiˇwn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck broˇ\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''quiˇwn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck broˇ\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`ˇ`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`ˇ`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``quiˇwn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck broˇ\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``quiˇwn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck broˇ\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck broˇ\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck broˇ\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} {"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} {"Key":"c"} {"Key":"i"} diff --git a/crates/vim/test_data/test_delete_e.json b/crates/vim/test_data/test_delete_e.json index ebbad8fc4d..85615e4d58 100644 --- a/crates/vim/test_data/test_delete_e.json +++ b/crates/vim/test_data/test_delete_e.json @@ -1,11 +1,3 @@ -{"Put":{"state":"Teˇst Test"}} -{"Key":"d"} -{"Key":"e"} -{"Get":{"state":"Teˇ Test","mode":"Normal"}} -{"Put":{"state":"Tˇest test"}} -{"Key":"d"} -{"Key":"e"} -{"Get":{"state":"Tˇ test","mode":"Normal"}} {"Put":{"state":"Test teˇst\ntest"}} {"Key":"d"} {"Key":"e"} diff --git a/crates/vim/test_data/test_delete_next_word_end.json b/crates/vim/test_data/test_delete_next_word_end.json new file mode 100644 index 0000000000..85615e4d58 --- /dev/null +++ b/crates/vim/test_data/test_delete_next_word_end.json @@ -0,0 +1,12 @@ +{"Put":{"state":"Test teˇst\ntest"}} +{"Key":"d"} +{"Key":"e"} +{"Get":{"state":"Test tˇe\ntest","mode":"Normal"}} +{"Put":{"state":"Test tesˇt\ntest"}} +{"Key":"d"} +{"Key":"e"} +{"Get":{"state":"Test teˇs","mode":"Normal"}} +{"Put":{"state":"Test teˇst-test test"}} +{"Key":"d"} +{"Key":"shift-e"} +{"Get":{"state":"Test teˇ test","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_surrounding_character_objects.json b/crates/vim/test_data/test_delete_surrounding_character_objects.json index 81b1f563e1..f273afba65 100644 --- a/crates/vim/test_data/test_delete_surrounding_character_objects.json +++ b/crates/vim/test_data/test_delete_surrounding_character_objects.json @@ -1,3 +1,1023 @@ +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'ˇ'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'ˇ'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''quiˇwn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck brˇo\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''quiˇwn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck brˇo\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`ˇ`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`ˇ`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``quiˇwn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck brˇo\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``quiˇwn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck brˇo\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck brˇo\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck brˇo\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} {"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} {"Key":"d"} {"Key":"i"} diff --git a/crates/vim/test_data/test_multiline_surrounding_character_objects.json b/crates/vim/test_data/test_multiline_surrounding_character_objects.json index cff3ab80e2..9758367c39 100644 --- a/crates/vim/test_data/test_multiline_surrounding_character_objects.json +++ b/crates/vim/test_data/test_multiline_surrounding_character_objects.json @@ -8,3 +8,8 @@ {"Key":"i"} {"Key":"{"} {"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":"Visual"}} +{"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n ˇreturn true\n }\n return false\n}"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"{"} +{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":"Visual"}} diff --git a/crates/vim/test_data/test_singleline_surrounding_character_objects.json b/crates/vim/test_data/test_singleline_surrounding_character_objects.json new file mode 100644 index 0000000000..f7f95ce697 --- /dev/null +++ b/crates/vim/test_data/test_singleline_surrounding_character_objects.json @@ -0,0 +1,27 @@ +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=12"}} +{"Put":{"state":"helˇlo \"world\"!"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"hello \"«worldˇ»\"!","mode":"Visual"}} +{"Put":{"state":"hello \"wˇorld\"!"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"hello \"«worldˇ»\"!","mode":"Visual"}} +{"Put":{"state":"hello \"wˇorld\"!"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"hello« \"world\"ˇ»!","mode":"Visual"}} +{"Put":{"state":"hello \"wˇorld\" !"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"hello «\"world\" ˇ»!","mode":"Visual"}} +{"Put":{"state":"hello \"wˇorld\"•\ngoodbye"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"hello «\"world\" ˇ»\ngoodbye","mode":"Visual"}} From 3cf98c4fae820b5c609d8498fa0726d27b8a8350 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sun, 22 Oct 2023 22:04:55 -0600 Subject: [PATCH 15/40] Add | as a bracket and a motion Although vim/nvim doesn't have | as brackets, it's common in langauges like Rust and Ruby, and I expect it to work. --- assets/keymaps/vim.json | 224 ++++-------------- crates/vim/src/motion.rs | 12 + crates/vim/src/object.rs | 62 ++++- ...ltiline_surrounding_character_objects.json | 2 +- 4 files changed, 119 insertions(+), 181 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index ea025747d8..81235bb72a 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -39,6 +39,7 @@ "w": "vim::NextWordStart", "{": "vim::StartOfParagraph", "}": "vim::EndOfParagraph", + "|": "vim::GoToColumn", "shift-w": [ "vim::NextWordStart", { @@ -97,14 +98,8 @@ "ctrl-o": "pane::GoBack", "ctrl-i": "pane::GoForward", "ctrl-]": "editor::GoToDefinition", - "escape": [ - "vim::SwitchMode", - "Normal" - ], - "ctrl+[": [ - "vim::SwitchMode", - "Normal" - ], + "escape": ["vim::SwitchMode", "Normal"], + "ctrl+[": ["vim::SwitchMode", "Normal"], "v": "vim::ToggleVisual", "shift-v": "vim::ToggleVisualLine", "ctrl-v": "vim::ToggleVisualBlock", @@ -233,123 +228,36 @@ } ], // Count support - "1": [ - "vim::Number", - 1 - ], - "2": [ - "vim::Number", - 2 - ], - "3": [ - "vim::Number", - 3 - ], - "4": [ - "vim::Number", - 4 - ], - "5": [ - "vim::Number", - 5 - ], - "6": [ - "vim::Number", - 6 - ], - "7": [ - "vim::Number", - 7 - ], - "8": [ - "vim::Number", - 8 - ], - "9": [ - "vim::Number", - 9 - ], + "1": ["vim::Number", 1], + "2": ["vim::Number", 2], + "3": ["vim::Number", 3], + "4": ["vim::Number", 4], + "5": ["vim::Number", 5], + "6": ["vim::Number", 6], + "7": ["vim::Number", 7], + "8": ["vim::Number", 8], + "9": ["vim::Number", 9], // window related commands (ctrl-w X) - "ctrl-w left": [ - "workspace::ActivatePaneInDirection", - "Left" - ], - "ctrl-w right": [ - "workspace::ActivatePaneInDirection", - "Right" - ], - "ctrl-w up": [ - "workspace::ActivatePaneInDirection", - "Up" - ], - "ctrl-w down": [ - "workspace::ActivatePaneInDirection", - "Down" - ], - "ctrl-w h": [ - "workspace::ActivatePaneInDirection", - "Left" - ], - "ctrl-w l": [ - "workspace::ActivatePaneInDirection", - "Right" - ], - "ctrl-w k": [ - "workspace::ActivatePaneInDirection", - "Up" - ], - "ctrl-w j": [ - "workspace::ActivatePaneInDirection", - "Down" - ], - "ctrl-w ctrl-h": [ - "workspace::ActivatePaneInDirection", - "Left" - ], - "ctrl-w ctrl-l": [ - "workspace::ActivatePaneInDirection", - "Right" - ], - "ctrl-w ctrl-k": [ - "workspace::ActivatePaneInDirection", - "Up" - ], - "ctrl-w ctrl-j": [ - "workspace::ActivatePaneInDirection", - "Down" - ], - "ctrl-w shift-left": [ - "workspace::SwapPaneInDirection", - "Left" - ], - "ctrl-w shift-right": [ - "workspace::SwapPaneInDirection", - "Right" - ], - "ctrl-w shift-up": [ - "workspace::SwapPaneInDirection", - "Up" - ], - "ctrl-w shift-down": [ - "workspace::SwapPaneInDirection", - "Down" - ], - "ctrl-w shift-h": [ - "workspace::SwapPaneInDirection", - "Left" - ], - "ctrl-w shift-l": [ - "workspace::SwapPaneInDirection", - "Right" - ], - "ctrl-w shift-k": [ - "workspace::SwapPaneInDirection", - "Up" - ], - "ctrl-w shift-j": [ - "workspace::SwapPaneInDirection", - "Down" - ], + "ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"], + "ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"], + "ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"], + "ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"], + "ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"], + "ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"], + "ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"], + "ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"], "ctrl-w g t": "pane::ActivateNextItem", "ctrl-w ctrl-g t": "pane::ActivateNextItem", "ctrl-w g shift-t": "pane::ActivatePrevItem", @@ -371,14 +279,8 @@ "ctrl-w ctrl-q": "pane::CloseAllItems", "ctrl-w o": "workspace::CloseInactiveTabsAndPanes", "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", - "ctrl-w n": [ - "workspace::NewFileInDirection", - "Up" - ], - "ctrl-w ctrl-n": [ - "workspace::NewFileInDirection", - "Up" - ] + "ctrl-w n": ["workspace::NewFileInDirection", "Up"], + "ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"] } }, { @@ -393,21 +295,12 @@ "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", "bindings": { ".": "vim::Repeat", - "c": [ - "vim::PushOperator", - "Change" - ], + "c": ["vim::PushOperator", "Change"], "shift-c": "vim::ChangeToEndOfLine", - "d": [ - "vim::PushOperator", - "Delete" - ], + "d": ["vim::PushOperator", "Delete"], "shift-d": "vim::DeleteToEndOfLine", "shift-j": "vim::JoinLines", - "y": [ - "vim::PushOperator", - "Yank" - ], + "y": ["vim::PushOperator", "Yank"], "shift-y": "vim::YankLine", "i": "vim::InsertBefore", "shift-i": "vim::InsertFirstNonWhitespace", @@ -443,10 +336,7 @@ "backwards": true } ], - "r": [ - "vim::PushOperator", - "Replace" - ], + "r": ["vim::PushOperator", "Replace"], "s": "vim::Substitute", "shift-s": "vim::SubstituteLine", "> >": "editor::Indent", @@ -458,10 +348,7 @@ { "context": "Editor && VimCount", "bindings": { - "0": [ - "vim::Number", - 0 - ] + "0": ["vim::Number", 0] } }, { @@ -497,12 +384,15 @@ "'": "vim::Quotes", "`": "vim::BackQuotes", "\"": "vim::DoubleQuotes", + "|": "vim::VerticalBars", "(": "vim::Parentheses", ")": "vim::Parentheses", + "b": "vim::Parentheses", "[": "vim::SquareBrackets", "]": "vim::SquareBrackets", "{": "vim::CurlyBrackets", "}": "vim::CurlyBrackets", + "shift-b": "vim::CurlyBrackets", "<": "vim::AngleBrackets", ">": "vim::AngleBrackets" } @@ -548,22 +438,10 @@ "shift-i": "vim::InsertBefore", "shift-a": "vim::InsertAfter", "shift-j": "vim::JoinLines", - "r": [ - "vim::PushOperator", - "Replace" - ], - "ctrl-c": [ - "vim::SwitchMode", - "Normal" - ], - "escape": [ - "vim::SwitchMode", - "Normal" - ], - "ctrl+[": [ - "vim::SwitchMode", - "Normal" - ], + "r": ["vim::PushOperator", "Replace"], + "ctrl-c": ["vim::SwitchMode", "Normal"], + "escape": ["vim::SwitchMode", "Normal"], + "ctrl+[": ["vim::SwitchMode", "Normal"], ">": "editor::Indent", "<": "editor::Outdent", "i": [ @@ -602,14 +480,8 @@ "bindings": { "tab": "vim::Tab", "enter": "vim::Enter", - "escape": [ - "vim::SwitchMode", - "Normal" - ], - "ctrl+[": [ - "vim::SwitchMode", - "Normal" - ] + "escape": ["vim::SwitchMode", "Normal"], + "ctrl+[": ["vim::SwitchMode", "Normal"] } }, { diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index e8d954bc13..92010b63c0 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -40,6 +40,7 @@ pub enum Motion { NextLineStart, StartOfLineDownward, EndOfLineDownward, + GoToColumn, } #[derive(Clone, Deserialize, PartialEq)] @@ -119,6 +120,7 @@ actions!( NextLineStart, StartOfLineDownward, EndOfLineDownward, + GoToColumn, ] ); impl_actions!( @@ -215,6 +217,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| { motion(Motion::EndOfLineDownward, cx) }); + cx.add_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx)); cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| { repeat_motion(action.backwards, cx) }) @@ -292,6 +295,7 @@ impl Motion { | Right | StartOfLine { .. } | EndOfLineDownward + | GoToColumn | NextWordStart { .. } | PreviousWordStart { .. } | FirstNonWhitespace { .. } @@ -317,6 +321,7 @@ impl Motion { | EndOfParagraph | StartOfLineDownward | EndOfLineDownward + | GoToColumn | NextWordStart { .. } | PreviousWordStart { .. } | FirstNonWhitespace { .. } @@ -346,6 +351,7 @@ impl Motion { | StartOfLineDownward | StartOfParagraph | EndOfParagraph + | GoToColumn | NextWordStart { .. } | PreviousWordStart { .. } | FirstNonWhitespace { .. } @@ -429,6 +435,7 @@ impl Motion { NextLineStart => (next_line_start(map, point, times), SelectionGoal::None), StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None), EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None), + GoToColumn => (go_to_column(map, point, times), SelectionGoal::None), }; (new_point != point || infallible).then_some((new_point, goal)) @@ -919,6 +926,11 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> first_non_whitespace(map, false, correct_line) } +fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { + let correct_line = start_of_relative_buffer_row(map, point, 0); + right(map, correct_line, times.saturating_sub(1)) +} + pub(crate) fn next_line_end( map: &DisplaySnapshot, mut point: DisplayPoint, diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index e4a0659f3f..6521390580 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -20,6 +20,7 @@ pub enum Object { Quotes, BackQuotes, DoubleQuotes, + VerticalBars, Parentheses, SquareBrackets, CurlyBrackets, @@ -40,6 +41,7 @@ actions!( Quotes, BackQuotes, DoubleQuotes, + VerticalBars, Parentheses, SquareBrackets, CurlyBrackets, @@ -64,6 +66,7 @@ pub fn init(cx: &mut AppContext) { }); cx.add_action(|_: &mut Workspace, _: &CurlyBrackets, cx: _| object(Object::CurlyBrackets, cx)); cx.add_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| object(Object::AngleBrackets, cx)); + cx.add_action(|_: &mut Workspace, _: &VerticalBars, cx: _| object(Object::VerticalBars, cx)); } fn object(object: Object, cx: &mut WindowContext) { @@ -79,9 +82,11 @@ fn object(object: Object, cx: &mut WindowContext) { impl Object { pub fn is_multiline(self) -> bool { match self { - Object::Word { .. } | Object::Quotes | Object::BackQuotes | Object::DoubleQuotes => { - false - } + Object::Word { .. } + | Object::Quotes + | Object::BackQuotes + | Object::VerticalBars + | Object::DoubleQuotes => false, Object::Sentence | Object::Parentheses | Object::AngleBrackets @@ -96,6 +101,7 @@ impl Object { Object::Quotes | Object::BackQuotes | Object::DoubleQuotes + | Object::VerticalBars | Object::Parentheses | Object::SquareBrackets | Object::CurlyBrackets @@ -111,6 +117,7 @@ impl Object { | Object::Quotes | Object::BackQuotes | Object::DoubleQuotes + | Object::VerticalBars | Object::Parentheses | Object::SquareBrackets | Object::CurlyBrackets @@ -142,6 +149,9 @@ impl Object { Object::DoubleQuotes => { surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"') } + Object::VerticalBars => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|') + } Object::Parentheses => { surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')') } @@ -568,7 +578,10 @@ fn surrounding_markers( mod test { use indoc::indoc; - use crate::test::{ExemptionFeatures, NeovimBackedTestContext}; + use crate::{ + state::Mode, + test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext}, + }; const WORD_LOCATIONS: &'static str = indoc! {" The quick ˇbrowˇnˇ••• @@ -940,6 +953,47 @@ mod test { .await; } + #[gpui::test] + async fn test_vertical_bars(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.set_state( + indoc! {" + fn boop() { + baz(ˇ|a, b| { bar(|j, k| { })}) + }" + }, + Mode::Normal, + ); + cx.simulate_keystrokes(["c", "i", "|"]); + cx.assert_state( + indoc! {" + fn boop() { + baz(|ˇ| { bar(|j, k| { })}) + }" + }, + Mode::Insert, + ); + cx.simulate_keystrokes(["escape", "1", "8", "|"]); + cx.assert_state( + indoc! {" + fn boop() { + baz(|| { bar(ˇ|j, k| { })}) + }" + }, + Mode::Normal, + ); + + cx.simulate_keystrokes(["v", "a", "|"]); + cx.assert_state( + indoc! {" + fn boop() { + baz(|| { bar(«|j, k| ˇ»{ })}) + }" + }, + Mode::Visual, + ); + } + #[gpui::test] async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/test_data/test_multiline_surrounding_character_objects.json b/crates/vim/test_data/test_multiline_surrounding_character_objects.json index 9758367c39..973df647a2 100644 --- a/crates/vim/test_data/test_multiline_surrounding_character_objects.json +++ b/crates/vim/test_data/test_multiline_surrounding_character_objects.json @@ -8,7 +8,7 @@ {"Key":"i"} {"Key":"{"} {"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":"Visual"}} -{"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n ˇreturn true\n }\n return false\n}"}} +{"Put":{"state":"func empty(a string) bool {\n if a == \"\" ˇ{\n return true\n }\n return false\n}"}} {"Key":"v"} {"Key":"i"} {"Key":"{"} From b495669c86317966a6ae303e354478e2af687d9c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sun, 22 Oct 2023 22:24:35 -0600 Subject: [PATCH 16/40] Fix neovim tests with angle brackets --- crates/vim/src/object.rs | 7 - .../src/test/neovim_backed_test_context.rs | 2 - crates/vim/src/test/neovim_connection.rs | 7 +- crates/vim/test_data/test_G.json | 1 - ..._change_surrounding_character_objects.json | 340 ++++++++++++++++++ crates/vim/test_data/test_delete_e.json | 12 - ..._delete_surrounding_character_objects.json | 338 +++++++++++++++++ crates/vim/test_data/test_e.json | 32 -- crates/vim/test_data/test_visual_paste.json | 26 -- 9 files changed, 684 insertions(+), 81 deletions(-) delete mode 100644 crates/vim/test_data/test_G.json delete mode 100644 crates/vim/test_data/test_delete_e.json delete mode 100644 crates/vim/test_data/test_e.json delete mode 100644 crates/vim/test_data/test_visual_paste.json diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 6521390580..2897d6fe91 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -816,10 +816,6 @@ mod test { let mut cx = NeovimBackedTestContext::new(cx).await; for (start, end) in SURROUNDING_OBJECTS { - if start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported() { - continue; - } - let marked_string = SURROUNDING_MARKER_STRING .replace('`', &start.to_string()) .replace('\'', &end.to_string()); @@ -999,9 +995,6 @@ mod test { let mut cx = NeovimBackedTestContext::new(cx).await; for (start, end) in SURROUNDING_OBJECTS { - if start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported() { - continue; - } let marked_string = SURROUNDING_MARKER_STRING .replace('`', &start.to_string()) .replace('\'', &end.to_string()); diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index a27676a8af..7944e9297c 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -34,8 +34,6 @@ pub enum ExemptionFeatures { AroundSentenceStartingBetweenIncludesWrongWhitespace, // Non empty selection with text objects in visual mode NonEmptyVisualTextObjects, - // Neovim freezes up for some reason with angle brackets - AngleBracketsFreezeNeovim, // Sentence Doesn't backtrack when its at the end of the file SentenceAfterPunctuationAtEndOfFile, } diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index ddcab39cb2..16c718b857 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -110,7 +110,12 @@ impl NeovimConnection { // Sends a keystroke to the neovim process. #[cfg(feature = "neovim")] pub async fn send_keystroke(&mut self, keystroke_text: &str) { - let keystroke = Keystroke::parse(keystroke_text).unwrap(); + let mut keystroke = Keystroke::parse(keystroke_text).unwrap(); + + if keystroke.key == "<" { + keystroke.key = "lt".to_string() + } + let special = keystroke.shift || keystroke.ctrl || keystroke.alt diff --git a/crates/vim/test_data/test_G.json b/crates/vim/test_data/test_G.json deleted file mode 100644 index de9e29e4aa..0000000000 --- a/crates/vim/test_data/test_G.json +++ /dev/null @@ -1 +0,0 @@ -[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,5],"end":[3,5]}}] \ No newline at end of file diff --git a/crates/vim/test_data/test_change_surrounding_character_objects.json b/crates/vim/test_data/test_change_surrounding_character_objects.json index a95fd8c56e..2e60b7729b 100644 --- a/crates/vim/test_data/test_change_surrounding_character_objects.json +++ b/crates/vim/test_data/test_change_surrounding_character_objects.json @@ -2038,3 +2038,343 @@ {"Key":"a"} {"Key":"}"} {"Get":{"state":"Th}e {}qui{ck bro}wn{\n}fox jumps ov{er\nthe lazy d}o{ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox juˇmps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>oe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"<"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <ˇ>quiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <ˇ>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <ˇ>quiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox juˇmps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>o"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoˇ"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":">"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovoe ˇquiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovoe ˇquiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe ˇquiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox juˇmps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>oe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"<"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e ˇquiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e ˇquiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e ˇquiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox juˇmps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>o"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoˇ"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":">"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_e.json b/crates/vim/test_data/test_delete_e.json deleted file mode 100644 index 85615e4d58..0000000000 --- a/crates/vim/test_data/test_delete_e.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"Test teˇst\ntest"}} -{"Key":"d"} -{"Key":"e"} -{"Get":{"state":"Test tˇe\ntest","mode":"Normal"}} -{"Put":{"state":"Test tesˇt\ntest"}} -{"Key":"d"} -{"Key":"e"} -{"Get":{"state":"Test teˇs","mode":"Normal"}} -{"Put":{"state":"Test teˇst-test test"}} -{"Key":"d"} -{"Key":"shift-e"} -{"Get":{"state":"Test teˇ test","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_surrounding_character_objects.json b/crates/vim/test_data/test_delete_surrounding_character_objects.json index f273afba65..a468b612d9 100644 --- a/crates/vim/test_data/test_delete_surrounding_character_objects.json +++ b/crates/vim/test_data/test_delete_surrounding_character_objects.json @@ -2032,3 +2032,341 @@ {"Key":"a"} {"Key":"}"} {"Get":{"state":"Th}e {}qui{ck bro}wn{\n}fox jumps ov{er\nthe lazy d}o{ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox juˇmps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>oe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"<"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <ˇ>quiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <ˇ>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <ˇ>quiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox juˇmps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>o"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoˇ"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":">"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovoe ˇquiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovoe ˇquiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe ˇquiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox juˇmps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>oe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"<"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e ˇquiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e ˇquiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e ˇquiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox juˇmps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>o"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoˇ"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":">"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} diff --git a/crates/vim/test_data/test_e.json b/crates/vim/test_data/test_e.json deleted file mode 100644 index 06f80dc245..0000000000 --- a/crates/vim/test_data/test_e.json +++ /dev/null @@ -1,32 +0,0 @@ -{"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"}} diff --git a/crates/vim/test_data/test_visual_paste.json b/crates/vim/test_data/test_visual_paste.json deleted file mode 100644 index a0ad377378..0000000000 --- a/crates/vim/test_data/test_visual_paste.json +++ /dev/null @@ -1,26 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"y"} -{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"p"} -{"Get":{"state":"The quick brown\nfox jjumpˇsumps over\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"d"} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"p"} -{"Get":{"state":"The quick brown\nthe \nˇfox jumps over\n dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lazy"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"d"} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} -{"Key":"k"} -{"Key":"shift-v"} -{"Key":"p"} -{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}} From 6e4e19d8fc29910a8e5945f20915750b4816a1b5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 23 Oct 2023 15:19:38 +0200 Subject: [PATCH 17/40] Fix infinite loop in select all --- crates/editor/src/editor.rs | 2 +- crates/vim/src/test.rs | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 82945fc00b..ef2c459aad 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6542,7 +6542,7 @@ impl Editor { { if selections .iter() - .find(|selection| selection.equals(&offset_range)) + .find(|selection| selection.range().overlaps(&offset_range)) .is_none() { next_selected_range = Some(offset_range); diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 4fb87e70a0..3b4e2fed60 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -734,3 +734,27 @@ async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) { two"}) .await; } + +#[gpui::test] +async fn test_select_all_issue_2170(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state( + indoc! {" + defmodule Test do + def test(a, ˇ[_, _] = b), do: IO.puts('hi') + end + "}, + Mode::Normal, + ); + cx.simulate_keystrokes(["g", "a"]); + // TODO: this would be better if it selected the [ not the space. + cx.assert_state( + indoc! {" + defmodule« ˇ»Test« ˇ»do + « ˇ»def« ˇ»test(a,« ˇ»[_,« ˇ»_]« ˇ»=« ˇ»b),« ˇ»do:« ˇ»IO.puts('hi') + end + "}, + Mode::Visual, + ); +} From 6c163afb8402b9861216b6ed7d2be2cfcf0ab9d8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 24 Oct 2023 09:15:35 +0200 Subject: [PATCH 18/40] bash: Add highlighting of ANSI c string (#3153) Fixes zed-industries/community#2169 Release Notes: - Fixed highlighting of ANSI C strings ($'foo') in "Shell script" language buffers. --- crates/zed/src/languages/bash/highlights.scm | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/zed/src/languages/bash/highlights.scm b/crates/zed/src/languages/bash/highlights.scm index a72c5468ed..5cb5dad6a0 100644 --- a/crates/zed/src/languages/bash/highlights.scm +++ b/crates/zed/src/languages/bash/highlights.scm @@ -3,6 +3,7 @@ (raw_string) (heredoc_body) (heredoc_start) + (ansi_c_string) ] @string (command_name) @function From 97a0864134a1c1d79a47c1aea5db7e440eb1500a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 24 Oct 2023 09:16:06 +0200 Subject: [PATCH 19/40] grammars: Update Bash grammar (#3155) Fixes zed-industries/community#2168 Release Notes: - Updated Bash (Shell script) Tree-sitter grammar (fixes zed-industries/community#2168) --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e817fed0db..04b329939d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8590,8 +8590,8 @@ dependencies = [ [[package]] name = "tree-sitter-bash" -version = "0.19.0" -source = "git+https://github.com/tree-sitter/tree-sitter-bash?rev=1b0321ee85701d5036c334a6f04761cdc672e64c#1b0321ee85701d5036c334a6f04761cdc672e64c" +version = "0.20.4" +source = "git+https://github.com/tree-sitter/tree-sitter-bash?rev=7331995b19b8f8aba2d5e26deb51d2195c18bc94#7331995b19b8f8aba2d5e26deb51d2195c18bc94" dependencies = [ "cc", "tree-sitter", diff --git a/Cargo.toml b/Cargo.toml index cf977b8fe6..836a0bd6b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,7 +125,7 @@ pretty_assertions = "1.3.0" git2 = { version = "0.15", default-features = false} uuid = { version = "1.1.2", features = ["v4"] } -tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" } +tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" } tree-sitter-c = "0.20.1" tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" } tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } From e6087e0ed9ecb1fc5ae898e6e30c8fe312fa2742 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 24 Oct 2023 09:46:46 +0200 Subject: [PATCH 20/40] Fix tests --- crates/collab/src/tests/channel_buffer_tests.rs | 5 +---- crates/collab/src/tests/channel_tests.rs | 14 +++++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index cb51c7c5f3..931610f5ff 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -436,10 +436,7 @@ async fn test_channel_buffer_disconnect( // Channel buffer observed the deletion channel_buffer_b.update(cx_b, |buffer, cx| { - assert_eq!( - buffer.channel(cx).unwrap().as_ref(), - &channel(channel_id, "the-channel", proto::ChannelRole::Member) - ); + assert!(buffer.channel(cx).is_none()); assert!(!buffer.is_connected()); }); } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 63cc78b651..85503f8ea3 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1,4 +1,5 @@ use crate::{ + db::{self, UserId}, rpc::RECONNECT_TIMEOUT, tests::{room_participants, RoomParticipants, TestServer}, }; @@ -292,11 +293,14 @@ async fn test_core_channels( server.disconnect_client(client_a.peer_id().unwrap()); deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - client_b - .channel_store() - .update(cx_b, |channel_store, cx| { - channel_store.rename(channel_a_id, "channel-a-renamed", cx) - }) + server + .app_state + .db + .rename_channel( + db::ChannelId::from_proto(channel_a_id), + UserId::from_proto(client_a.id()), + "channel-a-renamed", + ) .await .unwrap(); From 0e035c1a95d4c53172f30d4458bb5f06e7e3b927 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 24 Oct 2023 09:31:39 +0200 Subject: [PATCH 21/40] Fix character selection --- crates/editor/src/movement.rs | 6 ++++-- crates/language/src/buffer.rs | 2 +- crates/vim/src/test.rs | 5 ++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 580faf1050..5b780095d6 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -707,7 +707,9 @@ mod tests { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( surrounding_word(&snapshot, display_points[1]), - display_points[0]..display_points[2] + display_points[0]..display_points[2], + "{}", + marked_text.to_string() ); } @@ -717,7 +719,7 @@ mod tests { assert("loremˇ ˇ ˇipsum", cx); assert("lorem\nˇˇˇ\nipsum", cx); assert("lorem\nˇˇipsumˇ", cx); - assert("lorem,ˇˇ ˇipsum", cx); + assert("loremˇ,ˇˇ ipsum", cx); assert("ˇloremˇˇ, ipsum", cx); } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 063c7616a8..0194123bd2 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -373,8 +373,8 @@ pub(crate) struct DiagnosticEndpoint { #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] pub enum CharKind { - Punctuation, Whitespace, + Punctuation, Word, } diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 3b4e2fed60..9a6976183b 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -748,11 +748,10 @@ async fn test_select_all_issue_2170(cx: &mut gpui::TestAppContext) { Mode::Normal, ); cx.simulate_keystrokes(["g", "a"]); - // TODO: this would be better if it selected the [ not the space. cx.assert_state( indoc! {" - defmodule« ˇ»Test« ˇ»do - « ˇ»def« ˇ»test(a,« ˇ»[_,« ˇ»_]« ˇ»=« ˇ»b),« ˇ»do:« ˇ»IO.puts('hi') + defmodule Test do + def test(a, «[ˇ»_, _] = b), do: IO.puts('hi') end "}, Mode::Visual, From aa6990bb6b966c3424090b20fd4afa0ca87611db Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 24 Oct 2023 10:41:21 +0200 Subject: [PATCH 22/40] Fix set_channel_visibility for public channels --- crates/collab/src/db.rs | 1 + crates/collab/src/db/queries/channels.rs | 36 ++++++++----- crates/collab/src/rpc.rs | 12 ++--- crates/collab/src/tests/channel_tests.rs | 65 ++++++++++++++++++------ 4 files changed, 78 insertions(+), 36 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 8c6bca2113..f4e2602cc6 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -458,6 +458,7 @@ pub struct CreateChannelResult { pub struct SetChannelVisibilityResult { pub participants_to_update: HashMap, pub participants_to_remove: HashSet, + pub channels_to_remove: Vec, } #[derive(Debug)] diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 0abd36049d..9e7b1cabf5 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -244,9 +244,30 @@ impl Database { .into_iter() .collect(); + let mut channels_to_remove: Vec = vec![]; let mut participants_to_remove: HashSet = HashSet::default(); match visibility { ChannelVisibility::Members => { + let all_descendents: Vec = self + .get_channel_descendants(vec![channel_id], &*tx) + .await? + .into_iter() + .map(|edge| ChannelId::from_proto(edge.channel_id)) + .collect(); + + channels_to_remove = channel::Entity::find() + .filter( + channel::Column::Id + .is_in(all_descendents) + .and(channel::Column::Visibility.eq(ChannelVisibility::Public)), + ) + .all(&*tx) + .await? + .into_iter() + .map(|channel| channel.id) + .collect(); + + channels_to_remove.push(channel_id); for member in previous_members { if member.role.can_only_see_public_descendants() { participants_to_remove.insert(member.user_id); @@ -271,6 +292,7 @@ impl Database { Ok(SetChannelVisibilityResult { participants_to_update, participants_to_remove, + channels_to_remove, }) }) .await @@ -694,14 +716,10 @@ impl Database { .all(&*tx) .await?; - dbg!((user_id, &channel_memberships)); - let mut edges = self .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) .await?; - dbg!((user_id, &edges)); - let mut role_for_channel: HashMap = HashMap::default(); for membership in channel_memberships.iter() { @@ -710,8 +728,6 @@ impl Database { role_for_channel.insert(membership.channel_id, (membership.role, included)); } - dbg!((&role_for_channel, parent_channel_id)); - for ChannelEdge { parent_id, channel_id, @@ -739,7 +755,6 @@ impl Database { ); } } - dbg!((&role_for_channel, parent_channel_id)); let mut channels: Vec = Vec::new(); let mut channels_to_remove: HashSet = HashSet::default(); @@ -757,7 +772,6 @@ impl Database { || role == ChannelRole::Banned || role == ChannelRole::Guest && channel.visibility != ChannelVisibility::Public { - dbg!("remove", channel.id); channels_to_remove.insert(channel.id.0 as u64); continue; } @@ -865,8 +879,6 @@ impl Database { .get_channel_participant_details_internal(new_parent, &*tx) .await?; - dbg!(&members); - for member in members.iter() { if !member.role.can_see_all_descendants() { continue; @@ -897,8 +909,6 @@ impl Database { .await? }; - dbg!(&public_members); - for member in public_members { if !member.role.can_only_see_public_descendants() { continue; @@ -1666,8 +1676,6 @@ impl Database { .into_iter() .collect(); - dbg!(&participants_to_update); - let mut moved_channels: HashSet = HashSet::default(); moved_channels.insert(channel_id); for edge in self.get_channel_descendants([channel_id], &*tx).await? { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 2d89ec47a2..dda638e107 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2366,6 +2366,7 @@ async fn set_channel_visibility( let SetChannelVisibilityResult { participants_to_update, participants_to_remove, + channels_to_remove, } = db .set_channel_visibility(channel_id, visibility, session.user_id) .await?; @@ -2379,10 +2380,7 @@ async fn set_channel_visibility( } for user_id in participants_to_remove { let update = proto::UpdateChannels { - // for public participants we only need to remove the current channel - // (not descendants) - // because they can still see any public descendants - delete_channels: vec![channel_id.to_proto()], + delete_channels: channels_to_remove.iter().map(|id| id.to_proto()).collect(), ..Default::default() }; for connection_id in connection_pool.user_connection_ids(user_id) { @@ -2645,7 +2643,7 @@ async fn join_channel_internal( leave_room_for_session(&session).await?; let db = session.db().await; - let (joined_room, accept_invite_result, role) = db + let (joined_room, membership_updated, role) = db .join_channel( channel_id, session.user_id, @@ -2691,10 +2689,10 @@ async fn join_channel_internal( })?; let connection_pool = session.connection_pool().await; - if let Some(accept_invite_result) = accept_invite_result { + if let Some(membership_updated) = membership_updated { notify_membership_updated( &connection_pool, - accept_invite_result, + membership_updated, session.user_id, &session.peer, ); diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 85503f8ea3..8f3017287c 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1122,7 +1122,7 @@ async fn test_channel_link_notifications( assert_channels_list_shape( client_c.channel_store(), cx_c, - &[(zed_channel, 0), (helix_channel, 1)], + &[(zed_channel, 0)], ); } @@ -1250,44 +1250,79 @@ async fn test_guest_access( let client_b = server.create_client(cx_b, "user_b").await; let channels = server - .make_channel_tree(&[("channel-a", None)], (&client_a, cx_a)) + .make_channel_tree( + &[("channel-a", None), ("channel-b", Some("channel-a"))], + (&client_a, cx_a), + ) .await; - let channel_a_id = channels[0]; + let channel_a = channels[0]; + let channel_b = channels[1]; let active_call_b = cx_b.read(ActiveCall::global); - // should not be allowed to join + // Non-members should not be allowed to join assert!(active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_a_id, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_a, cx)) .await .is_err()); + // Make channels A and B public client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.set_channel_visibility(channel_a_id, proto::ChannelVisibility::Public, cx) + channel_store.set_channel_visibility(channel_a, proto::ChannelVisibility::Public, cx) + }) + .await + .unwrap(); + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_channel_visibility(channel_b, proto::ChannelVisibility::Public, cx) }) .await .unwrap(); + // Client B joins channel A as a guest active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_a_id, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_a, cx)) .await .unwrap(); deterministic.run_until_parked(); - - assert!(client_b - .channel_store() - .update(cx_b, |channel_store, _| channel_store - .channel_for_id(channel_a_id) - .is_some())); + assert_channels_list_shape( + client_a.channel_store(), + cx_a, + &[(channel_a, 0), (channel_b, 1)], + ); + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[(channel_a, 0), (channel_b, 1)], + ); client_a.channel_store().update(cx_a, |channel_store, _| { - let participants = channel_store.channel_participants(channel_a_id); + let participants = channel_store.channel_participants(channel_a); assert_eq!(participants.len(), 1); assert_eq!(participants[0].id, client_b.user_id().unwrap()); - }) + }); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_channel_visibility(channel_a, proto::ChannelVisibility::Members, cx) + }) + .await + .unwrap(); + + assert_channels_list_shape(client_b.channel_store(), cx_b, &[]); + + active_call_b + .update(cx_b, |call, cx| call.join_channel(channel_b, cx)) + .await + .unwrap(); + + deterministic.run_until_parked(); + assert_channels_list_shape(client_b.channel_store(), cx_b, &[(channel_b, 0)]); } #[gpui::test] From 3358420f6a3386893b3434c77fcbf81a2f79e601 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 24 Oct 2023 11:17:17 +0200 Subject: [PATCH 23/40] fix format --- crates/collab/src/tests/channel_tests.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 8f3017287c..5b0bf02f8f 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1119,11 +1119,7 @@ async fn test_channel_link_notifications( ) }); - assert_channels_list_shape( - client_c.channel_store(), - cx_c, - &[(zed_channel, 0)], - ); + assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]); } #[gpui::test] From 8ffe5a3ec7e560b5ac34893ea81f2719f8c80068 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 24 Oct 2023 13:26:37 +0200 Subject: [PATCH 24/40] move keychain access into semantic index as opposed to on init --- crates/ai/src/embedding.rs | 71 +++++++++--------- crates/project/src/project.rs | 12 +++ crates/semantic_index/src/embedding_queue.rs | 15 +++- crates/semantic_index/src/semantic_index.rs | 73 +++++++++++-------- .../src/semantic_index_tests.rs | 14 ++-- crates/zed/examples/semantic_index_eval.rs | 19 +---- script/evaluate_semantic_index | 2 +- 7 files changed, 114 insertions(+), 92 deletions(-) diff --git a/crates/ai/src/embedding.rs b/crates/ai/src/embedding.rs index 4d5e40fad9..b791414ba2 100644 --- a/crates/ai/src/embedding.rs +++ b/crates/ai/src/embedding.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::AsyncReadExt; use gpui::executor::Background; -use gpui::{serde_json, ViewContext}; +use gpui::{serde_json, AppContext}; use isahc::http::StatusCode; use isahc::prelude::Configurable; use isahc::{AsyncBody, Response}; @@ -89,7 +89,6 @@ impl Embedding { #[derive(Clone)] pub struct OpenAIEmbeddings { - pub api_key: Option, pub client: Arc, pub executor: Arc, rate_limit_count_rx: watch::Receiver>, @@ -123,8 +122,12 @@ struct OpenAIEmbeddingUsage { #[async_trait] pub trait EmbeddingProvider: Sync + Send { - fn is_authenticated(&self) -> bool; - async fn embed_batch(&self, spans: Vec) -> Result>; + fn retrieve_credentials(&self, cx: &AppContext) -> Option; + async fn embed_batch( + &self, + spans: Vec, + api_key: Option, + ) -> Result>; fn max_tokens_per_batch(&self) -> usize; fn truncate(&self, span: &str) -> (String, usize); fn rate_limit_expiration(&self) -> Option; @@ -134,13 +137,17 @@ pub struct DummyEmbeddings {} #[async_trait] impl EmbeddingProvider for DummyEmbeddings { - fn is_authenticated(&self) -> bool { - true + fn retrieve_credentials(&self, _cx: &AppContext) -> Option { + Some("Dummy API KEY".to_string()) } fn rate_limit_expiration(&self) -> Option { None } - async fn embed_batch(&self, spans: Vec) -> Result> { + async fn embed_batch( + &self, + spans: Vec, + _api_key: Option, + ) -> Result> { // 1024 is the OpenAI Embeddings size for ada models. // the model we will likely be starting with. let dummy_vec = Embedding::from(vec![0.32 as f32; 1536]); @@ -169,36 +176,11 @@ impl EmbeddingProvider for DummyEmbeddings { const OPENAI_INPUT_LIMIT: usize = 8190; impl OpenAIEmbeddings { - pub fn authenticate(&mut self, cx: &mut ViewContext) { - if self.api_key.is_none() { - let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { - Some(api_key) - } else if let Some((_, api_key)) = cx - .platform() - .read_credentials(OPENAI_API_URL) - .log_err() - .flatten() - { - String::from_utf8(api_key).log_err() - } else { - None - }; - - if let Some(api_key) = api_key { - self.api_key = Some(api_key); - } - } - } - pub fn new( - api_key: Option, - client: Arc, - executor: Arc, - ) -> Self { + pub fn new(client: Arc, executor: Arc) -> Self { let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None); let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx)); OpenAIEmbeddings { - api_key, client, executor, rate_limit_count_rx, @@ -264,8 +246,19 @@ impl OpenAIEmbeddings { #[async_trait] impl EmbeddingProvider for OpenAIEmbeddings { - fn is_authenticated(&self) -> bool { - self.api_key.is_some() + fn retrieve_credentials(&self, cx: &AppContext) -> Option { + if let Ok(api_key) = env::var("OPENAI_API_KEY") { + Some(api_key) + } else if let Some((_, api_key)) = cx + .platform() + .read_credentials(OPENAI_API_URL) + .log_err() + .flatten() + { + String::from_utf8(api_key).log_err() + } else { + None + } } fn max_tokens_per_batch(&self) -> usize { @@ -290,11 +283,15 @@ impl EmbeddingProvider for OpenAIEmbeddings { (output, tokens.len()) } - async fn embed_batch(&self, spans: Vec) -> Result> { + async fn embed_batch( + &self, + spans: Vec, + api_key: Option, + ) -> Result> { const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45]; const MAX_RETRIES: usize = 4; - let Some(api_key) = self.api_key.clone() else { + let Some(api_key) = api_key else { return Err(anyhow!("no open ai key provided")); }; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2eb1fd421c..7ade3675bc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -8489,6 +8489,18 @@ impl Project { } } + #[cfg(any(test, feature = "test-support"))] + fn install_default_formatters( + &self, + _worktree: Option, + _new_language: &Language, + _language_settings: &LanguageSettings, + _cx: &mut ModelContext, + ) -> Task> { + return Task::ready(Ok(())); + } + + #[cfg(not(any(test, feature = "test-support")))] fn install_default_formatters( &self, worktree: Option, diff --git a/crates/semantic_index/src/embedding_queue.rs b/crates/semantic_index/src/embedding_queue.rs index 6ae8faa4cd..d57d5c7bbe 100644 --- a/crates/semantic_index/src/embedding_queue.rs +++ b/crates/semantic_index/src/embedding_queue.rs @@ -41,6 +41,7 @@ pub struct EmbeddingQueue { pending_batch_token_count: usize, finished_files_tx: channel::Sender, finished_files_rx: channel::Receiver, + api_key: Option, } #[derive(Clone)] @@ -50,7 +51,11 @@ pub struct FileFragmentToEmbed { } impl EmbeddingQueue { - pub fn new(embedding_provider: Arc, executor: Arc) -> Self { + pub fn new( + embedding_provider: Arc, + executor: Arc, + api_key: Option, + ) -> Self { let (finished_files_tx, finished_files_rx) = channel::unbounded(); Self { embedding_provider, @@ -59,9 +64,14 @@ impl EmbeddingQueue { pending_batch_token_count: 0, finished_files_tx, finished_files_rx, + api_key, } } + pub fn set_api_key(&mut self, api_key: Option) { + self.api_key = api_key + } + pub fn push(&mut self, file: FileToEmbed) { if file.spans.is_empty() { self.finished_files_tx.try_send(file).unwrap(); @@ -108,6 +118,7 @@ impl EmbeddingQueue { let finished_files_tx = self.finished_files_tx.clone(); let embedding_provider = self.embedding_provider.clone(); + let api_key = self.api_key.clone(); self.executor .spawn(async move { @@ -132,7 +143,7 @@ impl EmbeddingQueue { return; }; - match embedding_provider.embed_batch(spans).await { + match embedding_provider.embed_batch(spans, api_key).await { Ok(embeddings) => { let mut embeddings = embeddings.into_iter(); for fragment in batch { diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index aae289e417..8839d25a84 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -7,10 +7,7 @@ pub mod semantic_index_settings; mod semantic_index_tests; use crate::semantic_index_settings::SemanticIndexSettings; -use ai::{ - completion::OPENAI_API_URL, - embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings}, -}; +use ai::embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings}; use anyhow::{anyhow, Result}; use collections::{BTreeMap, HashMap, HashSet}; use db::VectorDatabase; @@ -58,19 +55,6 @@ pub fn init( .join(Path::new(RELEASE_CHANNEL_NAME.as_str())) .join("embeddings_db"); - let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { - Some(api_key) - } else if let Some((_, api_key)) = cx - .platform() - .read_credentials(OPENAI_API_URL) - .log_err() - .flatten() - { - String::from_utf8(api_key).log_err() - } else { - None - }; - cx.subscribe_global::({ move |event, cx| { let Some(semantic_index) = SemanticIndex::global(cx) else { @@ -104,7 +88,7 @@ pub fn init( let semantic_index = SemanticIndex::new( fs, db_file_path, - Arc::new(OpenAIEmbeddings::new(api_key, http_client, cx.background())), + Arc::new(OpenAIEmbeddings::new(http_client, cx.background())), language_registry, cx.clone(), ) @@ -139,6 +123,8 @@ pub struct SemanticIndex { _embedding_task: Task<()>, _parsing_files_tasks: Vec>, projects: HashMap, ProjectState>, + api_key: Option, + embedding_queue: Arc>, } struct ProjectState { @@ -284,7 +270,7 @@ pub struct SearchResult { } impl SemanticIndex { - pub fn global(cx: &AppContext) -> Option> { + pub fn global(cx: &mut AppContext) -> Option> { if cx.has_global::>() { Some(cx.global::>().clone()) } else { @@ -292,12 +278,26 @@ impl SemanticIndex { } } + pub fn authenticate(&mut self, cx: &AppContext) { + if self.api_key.is_none() { + self.api_key = self.embedding_provider.retrieve_credentials(cx); + + self.embedding_queue + .lock() + .set_api_key(self.api_key.clone()); + } + } + + pub fn is_authenticated(&self) -> bool { + self.api_key.is_some() + } + pub fn enabled(cx: &AppContext) -> bool { settings::get::(cx).enabled } pub fn status(&self, project: &ModelHandle) -> SemanticIndexStatus { - if !self.embedding_provider.is_authenticated() { + if !self.is_authenticated() { return SemanticIndexStatus::NotAuthenticated; } @@ -339,7 +339,7 @@ impl SemanticIndex { Ok(cx.add_model(|cx| { let t0 = Instant::now(); let embedding_queue = - EmbeddingQueue::new(embedding_provider.clone(), cx.background().clone()); + EmbeddingQueue::new(embedding_provider.clone(), cx.background().clone(), None); let _embedding_task = cx.background().spawn({ let embedded_files = embedding_queue.finished_files(); let db = db.clone(); @@ -404,6 +404,8 @@ impl SemanticIndex { _embedding_task, _parsing_files_tasks, projects: Default::default(), + api_key: None, + embedding_queue } })) } @@ -718,12 +720,13 @@ impl SemanticIndex { let index = self.index_project(project.clone(), cx); let embedding_provider = self.embedding_provider.clone(); + let api_key = self.api_key.clone(); cx.spawn(|this, mut cx| async move { index.await?; let t0 = Instant::now(); let query = embedding_provider - .embed_batch(vec![query]) + .embed_batch(vec![query], api_key) .await? .pop() .ok_or_else(|| anyhow!("could not embed query"))?; @@ -941,6 +944,7 @@ impl SemanticIndex { let fs = self.fs.clone(); let db_path = self.db.path().clone(); let background = cx.background().clone(); + let api_key = self.api_key.clone(); cx.background().spawn(async move { let db = VectorDatabase::new(fs, db_path.clone(), background).await?; let mut results = Vec::::new(); @@ -955,10 +959,15 @@ impl SemanticIndex { .parse_file_with_template(None, &snapshot.text(), language) .log_err() .unwrap_or_default(); - if Self::embed_spans(&mut spans, embedding_provider.as_ref(), &db) - .await - .log_err() - .is_some() + if Self::embed_spans( + &mut spans, + embedding_provider.as_ref(), + &db, + api_key.clone(), + ) + .await + .log_err() + .is_some() { for span in spans { let similarity = span.embedding.unwrap().similarity(&query); @@ -998,8 +1007,11 @@ impl SemanticIndex { project: ModelHandle, cx: &mut ModelContext, ) -> Task> { - if !self.embedding_provider.is_authenticated() { - return Task::ready(Err(anyhow!("user is not authenticated"))); + if self.api_key.is_none() { + self.authenticate(cx); + if self.api_key.is_none() { + return Task::ready(Err(anyhow!("user is not authenticated"))); + } } if !self.projects.contains_key(&project.downgrade()) { @@ -1180,6 +1192,7 @@ impl SemanticIndex { spans: &mut [Span], embedding_provider: &dyn EmbeddingProvider, db: &VectorDatabase, + api_key: Option, ) -> Result<()> { let mut batch = Vec::new(); let mut batch_tokens = 0; @@ -1202,7 +1215,7 @@ impl SemanticIndex { if batch_tokens + span.token_count > embedding_provider.max_tokens_per_batch() { let batch_embeddings = embedding_provider - .embed_batch(mem::take(&mut batch)) + .embed_batch(mem::take(&mut batch), api_key.clone()) .await?; embeddings.extend(batch_embeddings); batch_tokens = 0; @@ -1214,7 +1227,7 @@ impl SemanticIndex { if !batch.is_empty() { let batch_embeddings = embedding_provider - .embed_batch(mem::take(&mut batch)) + .embed_batch(mem::take(&mut batch), api_key) .await?; embeddings.extend(batch_embeddings); diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 182010ca83..a1ee3e5ada 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -7,7 +7,7 @@ use crate::{ use ai::embedding::{DummyEmbeddings, Embedding, EmbeddingProvider}; use anyhow::Result; use async_trait::async_trait; -use gpui::{executor::Deterministic, Task, TestAppContext}; +use gpui::{executor::Deterministic, AppContext, Task, TestAppContext}; use language::{Language, LanguageConfig, LanguageRegistry, ToOffset}; use parking_lot::Mutex; use pretty_assertions::assert_eq; @@ -228,7 +228,7 @@ async fn test_embedding_batching(cx: &mut TestAppContext, mut rng: StdRng) { let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); - let mut queue = EmbeddingQueue::new(embedding_provider.clone(), cx.background()); + let mut queue = EmbeddingQueue::new(embedding_provider.clone(), cx.background(), None); for file in &files { queue.push(file.clone()); } @@ -1281,8 +1281,8 @@ impl FakeEmbeddingProvider { #[async_trait] impl EmbeddingProvider for FakeEmbeddingProvider { - fn is_authenticated(&self) -> bool { - true + fn retrieve_credentials(&self, _cx: &AppContext) -> Option { + Some("Fake Credentials".to_string()) } fn truncate(&self, span: &str) -> (String, usize) { (span.to_string(), 1) @@ -1296,7 +1296,11 @@ impl EmbeddingProvider for FakeEmbeddingProvider { None } - async fn embed_batch(&self, spans: Vec) -> Result> { + async fn embed_batch( + &self, + spans: Vec, + _api_key: Option, + ) -> Result> { self.embedding_count .fetch_add(spans.len(), atomic::Ordering::SeqCst); Ok(spans.iter().map(|span| self.embed_sync(span)).collect()) diff --git a/crates/zed/examples/semantic_index_eval.rs b/crates/zed/examples/semantic_index_eval.rs index 73b3b9987b..e750307800 100644 --- a/crates/zed/examples/semantic_index_eval.rs +++ b/crates/zed/examples/semantic_index_eval.rs @@ -1,4 +1,3 @@ -use ai::completion::OPENAI_API_URL; use ai::embedding::OpenAIEmbeddings; use anyhow::{anyhow, Result}; use client::{self, UserStore}; @@ -18,7 +17,6 @@ use std::{cmp, env, fs}; use util::channel::{RELEASE_CHANNEL, RELEASE_CHANNEL_NAME}; use util::http::{self}; use util::paths::EMBEDDINGS_DIR; -use util::ResultExt; use zed::languages; #[derive(Deserialize, Clone, Serialize)] @@ -57,7 +55,7 @@ fn parse_eval() -> anyhow::Result> { .as_path() .parent() .unwrap() - .join("crates/semantic_index/eval"); + .join("zed/crates/semantic_index/eval"); let mut repo_evals: Vec = Vec::new(); for entry in fs::read_dir(eval_folder)? { @@ -472,25 +470,12 @@ fn main() { let languages = languages.clone(); - let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { - Some(api_key) - } else if let Some((_, api_key)) = cx - .platform() - .read_credentials(OPENAI_API_URL) - .log_err() - .flatten() - { - String::from_utf8(api_key).log_err() - } else { - None - }; - let fs = fs.clone(); cx.spawn(|mut cx| async move { let semantic_index = SemanticIndex::new( fs.clone(), db_file_path, - Arc::new(OpenAIEmbeddings::new(api_key, http_client, cx.background())), + Arc::new(OpenAIEmbeddings::new(http_client, cx.background())), languages.clone(), cx.clone(), ) diff --git a/script/evaluate_semantic_index b/script/evaluate_semantic_index index 8dcb53c399..9ecfe898c5 100755 --- a/script/evaluate_semantic_index +++ b/script/evaluate_semantic_index @@ -1,3 +1,3 @@ #!/bin/bash -RUST_LOG=semantic_index=trace cargo run -p semantic_index --example eval --release +RUST_LOG=semantic_index=trace cargo run --example semantic_index_eval --release From 0dd45bbf21ee0554f5f0a27996a2cff06319689f Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 24 Oct 2023 13:35:28 +0200 Subject: [PATCH 25/40] fully qualify paths inside conditional compilation methods --- crates/project/src/project.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7ade3675bc..b189134ec3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -53,7 +53,7 @@ use lsp::{ use lsp_command::*; use node_runtime::NodeRuntime; use postage::watch; -use prettier::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS}; +use prettier::{LocateStart, Prettier}; use project_settings::{LspSettings, ProjectSettings}; use rand::prelude::*; use search::SearchQuery; @@ -79,13 +79,10 @@ use std::{ time::{Duration, Instant}, }; use terminals::Terminals; -use text::{Anchor, LineEnding, Rope}; +use text::Anchor; use util::{ - debug_panic, defer, - http::HttpClient, - merge_json_value_into, - paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH}, - post_inc, ResultExt, TryFutureExt as _, + debug_panic, defer, http::HttpClient, merge_json_value_into, + paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, }; pub use fs::*; @@ -8531,7 +8528,7 @@ impl Project { return Task::ready(Ok(())); }; - let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path(); + let default_prettier_dir = util::paths::DEFAULT_PRETTIER_DIR.as_path(); let already_running_prettier = self .prettier_instances .get(&(worktree, default_prettier_dir.to_path_buf())) @@ -8540,10 +8537,10 @@ impl Project { let fs = Arc::clone(&self.fs); cx.background() .spawn(async move { - let prettier_wrapper_path = default_prettier_dir.join(PRETTIER_SERVER_FILE); + let prettier_wrapper_path = default_prettier_dir.join(prettier::PRETTIER_SERVER_FILE); // method creates parent directory if it doesn't exist - fs.save(&prettier_wrapper_path, &Rope::from(PRETTIER_SERVER_JS), LineEnding::Unix).await - .with_context(|| format!("writing {PRETTIER_SERVER_FILE} file at {prettier_wrapper_path:?}"))?; + fs.save(&prettier_wrapper_path, &text::Rope::from(prettier::PRETTIER_SERVER_JS), text::LineEnding::Unix).await + .with_context(|| format!("writing {prettier::PRETTIER_SERVER_FILE} file at {prettier_wrapper_path:?}"))?; let packages_to_versions = future::try_join_all( prettier_plugins From feefb8d063d6622d349af9a59d3ceb8503f32668 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 24 Oct 2023 13:37:34 +0200 Subject: [PATCH 26/40] fixed format! call for prettier:: --- crates/project/src/project.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b189134ec3..cb238a8673 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -8540,7 +8540,7 @@ impl Project { let prettier_wrapper_path = default_prettier_dir.join(prettier::PRETTIER_SERVER_FILE); // method creates parent directory if it doesn't exist fs.save(&prettier_wrapper_path, &text::Rope::from(prettier::PRETTIER_SERVER_JS), text::LineEnding::Unix).await - .with_context(|| format!("writing {prettier::PRETTIER_SERVER_FILE} file at {prettier_wrapper_path:?}"))?; + .with_context(|| format!("writing {} file at {prettier_wrapper_path:?}", prettier::PRETTIER_SERVER_FILE))?; let packages_to_versions = future::try_join_all( prettier_plugins From e9ce9359910c57ab77ede72f8844558342b2a408 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 24 Oct 2023 14:25:46 +0200 Subject: [PATCH 27/40] Rework prettier tests Do not infuse `FakeNodeRuntime` with prettier exceptions, rather keep the default formatter installation method as no-op. --- crates/collab/src/tests/integration_tests.rs | 6 +- crates/editor/src/editor_tests.rs | 6 +- crates/node_runtime/src/node_runtime.rs | 105 ++----------------- crates/prettier/src/prettier.rs | 8 +- crates/project/src/project.rs | 12 +-- 5 files changed, 16 insertions(+), 121 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 8396e8947f..ea8c1bd4b3 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4555,11 +4555,7 @@ async fn test_prettier_formatting_buffer( .insert_tree(&directory, json!({ "a.rs": buffer_text })) .await; let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await; - let prettier_format_suffix = project_a.update(cx_a, |project, _| { - let suffix = project.enable_test_prettier(&[test_plugin]); - project.languages().add(language); - suffix - }); + let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; let buffer_a = cx_a .background() .spawn(project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6421fc6f7a..d5738ec9f6 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5117,7 +5117,6 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; project.update(cx, |project, _| { - project.enable_test_prettier(&[]); project.languages().add(Arc::new(language)); }); let buffer = project @@ -7864,10 +7863,9 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { fs.insert_file("/file.rs", Default::default()).await; let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - let prettier_format_suffix = project.update(cx, |project, _| { - let suffix = project.enable_test_prettier(&[test_plugin]); + let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; + project.update(cx, |project, _| { project.languages().add(Arc::new(language)); - suffix }); let buffer = project .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index dcb8833f8c..a099a025e6 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -220,96 +220,31 @@ impl NodeRuntime for RealNodeRuntime { } } -pub struct FakeNodeRuntime(Option); - -struct PrettierSupport { - plugins: Vec<&'static str>, -} +pub struct FakeNodeRuntime; impl FakeNodeRuntime { pub fn new() -> Arc { - Arc::new(FakeNodeRuntime(None)) - } - - pub fn with_prettier_support(plugins: &[&'static str]) -> Arc { - Arc::new(FakeNodeRuntime(Some(PrettierSupport::new(plugins)))) + Arc::new(Self) } } #[async_trait::async_trait] impl NodeRuntime for FakeNodeRuntime { async fn binary_path(&self) -> anyhow::Result { - if let Some(prettier_support) = &self.0 { - prettier_support.binary_path().await - } else { - unreachable!() - } + unreachable!() } async fn run_npm_subcommand( &self, - directory: Option<&Path>, + _: Option<&Path>, subcommand: &str, args: &[&str], ) -> anyhow::Result { - if let Some(prettier_support) = &self.0 { - prettier_support - .run_npm_subcommand(directory, subcommand, args) - .await - } else { - unreachable!() - } + unreachable!("Should not run npm subcommand '{subcommand}' with args {args:?}") } async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result { - if let Some(prettier_support) = &self.0 { - prettier_support.npm_package_latest_version(name).await - } else { - unreachable!() - } - } - - async fn npm_install_packages( - &self, - directory: &Path, - packages: &[(&str, &str)], - ) -> anyhow::Result<()> { - if let Some(prettier_support) = &self.0 { - prettier_support - .npm_install_packages(directory, packages) - .await - } else { - unreachable!() - } - } -} - -impl PrettierSupport { - const PACKAGE_VERSION: &str = "0.0.1"; - - fn new(plugins: &[&'static str]) -> Self { - Self { - plugins: plugins.to_vec(), - } - } -} - -#[async_trait::async_trait] -impl NodeRuntime for PrettierSupport { - async fn binary_path(&self) -> anyhow::Result { - Ok(PathBuf::from("prettier_fake_node")) - } - - async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result { - unreachable!() - } - - async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result { - if name == "prettier" || self.plugins.contains(&name) { - Ok(Self::PACKAGE_VERSION.to_string()) - } else { - panic!("Unexpected package name: {name}") - } + unreachable!("Should not query npm package '{name}' for latest version") } async fn npm_install_packages( @@ -317,32 +252,6 @@ impl NodeRuntime for PrettierSupport { _: &Path, packages: &[(&str, &str)], ) -> anyhow::Result<()> { - assert_eq!( - packages.len(), - self.plugins.len() + 1, - "Unexpected packages length to install: {:?}, expected `prettier` + {:?}", - packages, - self.plugins - ); - for (name, version) in packages { - assert!( - name == &"prettier" || self.plugins.contains(name), - "Unexpected package `{}` to install in packages {:?}, expected {} for `prettier` + {:?}", - name, - packages, - Self::PACKAGE_VERSION, - self.plugins - ); - assert_eq!( - version, - &Self::PACKAGE_VERSION, - "Unexpected package version `{}` to install in packages {:?}, expected {} for `prettier` + {:?}", - version, - packages, - Self::PACKAGE_VERSION, - self.plugins - ); - } - Ok(()) + unreachable!("Should not install packages {packages:?}") } } diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 09b793e5a2..bddfcb3a8f 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -44,6 +44,9 @@ pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); const PRETTIER_PACKAGE_NAME: &str = "prettier"; const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss"; +#[cfg(any(test, feature = "test-support"))] +pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier"; + impl Prettier { pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[ ".prettierrc", @@ -60,9 +63,6 @@ impl Prettier { ".editorconfig", ]; - #[cfg(any(test, feature = "test-support"))] - pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier"; - pub async fn locate( starting_path: Option, fs: Arc, @@ -349,7 +349,7 @@ impl Prettier { #[cfg(any(test, feature = "test-support"))] Self::Test(_) => Ok(buffer .read_with(cx, |buffer, cx| { - let formatted_text = buffer.text() + Self::FORMAT_SUFFIX; + let formatted_text = buffer.text() + FORMAT_SUFFIX; buffer.diff(formatted_text, cx) }) .await), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index cb238a8673..fd21c64945 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -86,6 +86,8 @@ use util::{ }; pub use fs::*; +#[cfg(any(test, feature = "test-support"))] +pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX; pub use worktree::*; pub trait Item { @@ -833,16 +835,6 @@ impl Project { project } - /// Enables a prettier mock that avoids interacting with node runtime, prettier LSP wrapper, or any real file changes. - /// Instead, if appends the suffix to every input, this suffix is returned by this method. - #[cfg(any(test, feature = "test-support"))] - pub fn enable_test_prettier(&mut self, plugins: &[&'static str]) -> &'static str { - self.node = Some(node_runtime::FakeNodeRuntime::with_prettier_support( - plugins, - )); - Prettier::FORMAT_SUFFIX - } - fn on_settings_changed(&mut self, cx: &mut ModelContext) { let mut language_servers_to_start = Vec::new(); let mut language_formatters_to_check = Vec::new(); From 463b24949ee341c5d387096c40004fa79f1c72b3 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 24 Oct 2023 09:22:58 -0400 Subject: [PATCH 28/40] Tweak notification styles --- styles/src/style_tree/notification_panel.ts | 43 +++++++++------------ 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/styles/src/style_tree/notification_panel.ts b/styles/src/style_tree/notification_panel.ts index 3b6a87946a..ecafc3c8e6 100644 --- a/styles/src/style_tree/notification_panel.ts +++ b/styles/src/style_tree/notification_panel.ts @@ -1,12 +1,22 @@ import { background, border, text } from "./components" import { icon_button } from "../component/icon_button" -import { useTheme } from "../theme" -import { interactive } from "../element" +import { useTheme, with_opacity } from "../theme" +import { text_button } from "../component" export default function (): any { const theme = useTheme() const layer = theme.middle + const notification_text = { + padding: { top: 4, bottom: 4 }, + ...text(layer, "sans", "base"), + } + + const notification_read_text_color = with_opacity( + theme.middle.base.default.foreground, + 0.6 + ) + return { background: background(layer), avatar: { @@ -31,34 +41,19 @@ export default function (): any { }, }, read_text: { - padding: { top: 4, bottom: 4 }, - ...text(layer, "sans", "disabled"), + ...notification_text, + color: notification_read_text_color, }, - unread_text: { - padding: { top: 4, bottom: 4 }, - ...text(layer, "sans", "base"), - }, - button: interactive({ - base: { - ...text(theme.lowest, "sans", "on", { size: "xs" }), - background: background(theme.lowest, "on"), - padding: 4, - corner_radius: 6, - margin: { left: 6 }, - }, - - state: { - hovered: { - background: background(theme.lowest, "on", "hovered"), - }, - }, + unread_text: notification_text, + button: text_button({ + variant: "ghost", }), timestamp: text(layer, "sans", "base", "disabled"), avatar_container: { padding: { - right: 6, + right: 8, left: 2, - top: 2, + top: 4, bottom: 2, }, }, From c9e670397f86e9a78bddaf22c09d5bee48a9eecc Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 24 Oct 2023 09:25:49 -0400 Subject: [PATCH 29/40] Give notifications a bit more breathing room by default --- assets/settings/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index e70b563359..19c73ca021 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -148,7 +148,7 @@ // Where to dock channels panel. Can be 'left' or 'right'. "dock": "right", // Default width of the channels panel. - "default_width": 240 + "default_width": 380 }, "assistant": { // Whether to show the assistant panel button in the status bar. From beb91fa0948346e0a53c8ef6e24e4b8938e6f495 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 24 Oct 2023 09:51:26 -0400 Subject: [PATCH 30/40] Add `meta_text` to style metaline --- crates/collab_ui/src/notification_panel.rs | 2 +- crates/theme/src/theme.rs | 1 + styles/src/style_tree/notification_panel.ts | 7 +++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index e245a919f3..80c99b5c72 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -257,7 +257,7 @@ impl NotificationPanel { } else { "You declined" }, - style.read_text.text.clone(), + style.meta_text.text.clone(), ) .flex_float() .into_any(), diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 3f4264886f..c29f23c63a 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -674,6 +674,7 @@ pub struct NotificationPanel { pub icon_button: Interactive, pub unread_text: ContainedText, pub read_text: ContainedText, + pub meta_text: ContainedText, pub timestamp: ContainedText, pub button: Interactive, } diff --git a/styles/src/style_tree/notification_panel.ts b/styles/src/style_tree/notification_panel.ts index ecafc3c8e6..6bc2b9c611 100644 --- a/styles/src/style_tree/notification_panel.ts +++ b/styles/src/style_tree/notification_panel.ts @@ -44,6 +44,13 @@ export default function (): any { ...notification_text, color: notification_read_text_color, }, + meta_text: { + padding: { top: 4, bottom: 4, right: 4 }, + ...text(layer, "sans", "base"), + color: with_opacity( + theme.middle.base.default.foreground, + 0.6) + }, unread_text: notification_text, button: text_button({ variant: "ghost", From c8dfccff36fcf9a143211d31b78bd1136645f646 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 24 Oct 2023 09:57:33 -0400 Subject: [PATCH 31/40] Revert "Add `meta_text` to style metaline" This reverts commit beb91fa0948346e0a53c8ef6e24e4b8938e6f495. --- crates/collab_ui/src/notification_panel.rs | 2 +- crates/theme/src/theme.rs | 1 - styles/src/style_tree/notification_panel.ts | 7 ------- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 80c99b5c72..e245a919f3 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -257,7 +257,7 @@ impl NotificationPanel { } else { "You declined" }, - style.meta_text.text.clone(), + style.read_text.text.clone(), ) .flex_float() .into_any(), diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c29f23c63a..3f4264886f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -674,7 +674,6 @@ pub struct NotificationPanel { pub icon_button: Interactive, pub unread_text: ContainedText, pub read_text: ContainedText, - pub meta_text: ContainedText, pub timestamp: ContainedText, pub button: Interactive, } diff --git a/styles/src/style_tree/notification_panel.ts b/styles/src/style_tree/notification_panel.ts index 6bc2b9c611..ecafc3c8e6 100644 --- a/styles/src/style_tree/notification_panel.ts +++ b/styles/src/style_tree/notification_panel.ts @@ -44,13 +44,6 @@ export default function (): any { ...notification_text, color: notification_read_text_color, }, - meta_text: { - padding: { top: 4, bottom: 4, right: 4 }, - ...text(layer, "sans", "base"), - color: with_opacity( - theme.middle.base.default.foreground, - 0.6) - }, unread_text: notification_text, button: text_button({ variant: "ghost", From 5c03b6a610911726bf4c7085088588c18b0cce5c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 24 Oct 2023 17:28:03 +0200 Subject: [PATCH 32/40] Remove logic for multiple channel parents Co-authored-by: Conrad Co-authored-by: Kyle Co-authored-by: Joseph --- crates/channel/src/channel.rs | 4 +- crates/channel/src/channel_store.rs | 157 +-- .../src/channel_store/channel_index.rs | 120 +- crates/channel/src/channel_store_tests.rs | 32 +- .../20221109000000_test_schema.sql | 11 +- ...6_move_channel_paths_to_channels_table.sql | 12 + crates/collab/src/db.rs | 17 +- crates/collab/src/db/queries/buffers.rs | 18 +- crates/collab/src/db/queries/channels.rs | 1011 +++++------------ crates/collab/src/db/queries/messages.rs | 79 +- crates/collab/src/db/queries/rooms.rs | 43 +- crates/collab/src/db/tables.rs | 1 - crates/collab/src/db/tables/channel.rs | 22 + crates/collab/src/db/tables/channel_path.rs | 15 - crates/collab/src/db/tests.rs | 29 +- crates/collab/src/db/tests/channel_tests.rs | 600 +++------- crates/collab/src/rpc.rs | 60 +- .../collab/src/tests/channel_buffer_tests.rs | 23 +- crates/collab/src/tests/channel_tests.rs | 198 +--- .../src/tests/random_channel_buffer_tests.rs | 4 +- crates/collab_ui/src/collab_panel.rs | 358 +++--- crates/rpc/proto/zed.proto | 23 +- crates/rpc/src/proto.rs | 4 - 23 files changed, 772 insertions(+), 2069 deletions(-) create mode 100644 crates/collab/migrations/20231024085546_move_channel_paths_to_channels_table.sql delete mode 100644 crates/collab/src/db/tables/channel_path.rs diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index b6db304a70..d0a32e16ff 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -11,9 +11,7 @@ pub use channel_chat::{ mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, MessageParams, }; -pub use channel_store::{ - Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore, -}; +pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelStore}; #[cfg(test)] mod channel_store_tests; diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 2665c2e1ec..14738e170b 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -9,11 +9,10 @@ use db::RELEASE_CHANNEL; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{ - proto::{self, ChannelEdge, ChannelVisibility}, + proto::{self, ChannelVisibility}, TypedEnvelope, }; -use serde_derive::{Deserialize, Serialize}; -use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration}; +use std::{mem, sync::Arc, time::Duration}; use util::ResultExt; pub fn init(client: &Arc, user_store: ModelHandle, cx: &mut AppContext) { @@ -27,7 +26,7 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub type ChannelId = u64; pub struct ChannelStore { - channel_index: ChannelIndex, + pub channel_index: ChannelIndex, channel_invitations: Vec>, channel_participants: HashMap>>, outgoing_invites: HashSet<(ChannelId, UserId)>, @@ -42,8 +41,6 @@ pub struct ChannelStore { _update_channels: Task<()>, } -pub type ChannelData = (Channel, ChannelPath); - #[derive(Clone, Debug, PartialEq)] pub struct Channel { pub id: ChannelId, @@ -52,6 +49,7 @@ pub struct Channel { pub role: proto::ChannelRole, pub unseen_note_version: Option<(u64, clock::Global)>, pub unseen_message_id: Option, + pub parent_path: Vec, } impl Channel { @@ -78,9 +76,6 @@ impl Channel { } } -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] -pub struct ChannelPath(Arc<[ChannelId]>); - pub struct ChannelMembership { pub user: Arc, pub kind: proto::channel_member::Kind, @@ -193,14 +188,11 @@ impl ChannelStore { self.client.clone() } - pub fn has_children(&self, channel_id: ChannelId) -> bool { - self.channel_index.iter().any(|path| { - if let Some(ix) = path.iter().position(|id| *id == channel_id) { - path.len() > ix + 1 - } else { - false - } - }) + pub fn channel_has_children(&self) -> bool { + self.channel_index + .by_id() + .iter() + .any(|(_, channel)| channel.parent_path.contains(&channel.id)) } /// Returns the number of unique channels in the store @@ -222,20 +214,19 @@ impl ChannelStore { } /// Iterate over all entries in the channel DAG - pub fn channel_dag_entries(&self) -> impl '_ + Iterator)> { - self.channel_index.iter().map(move |path| { - let id = path.last().unwrap(); - let channel = self.channel_for_id(*id).unwrap(); - (path.len() - 1, channel) - }) + pub fn ordered_channels(&self) -> impl '_ + Iterator)> { + self.channel_index + .ordered_channels() + .iter() + .filter_map(move |id| { + let channel = self.channel_index.by_id().get(id)?; + Some((channel.parent_path.len(), channel)) + }) } - pub fn channel_dag_entry_at(&self, ix: usize) -> Option<(&Arc, &ChannelPath)> { - let path = self.channel_index.get(ix)?; - let id = path.last().unwrap(); - let channel = self.channel_for_id(*id).unwrap(); - - Some((channel, path)) + pub fn channel_at_index(&self, ix: usize) -> Option<&Arc> { + let channel_id = self.channel_index.ordered_channels().get(ix)?; + self.channel_index.by_id().get(channel_id) } pub fn channel_at(&self, ix: usize) -> Option<&Arc> { @@ -484,20 +475,19 @@ impl ChannelStore { .ok_or_else(|| anyhow!("missing channel in response"))?; let channel_id = channel.id; - let parent_edge = if let Some(parent_id) = parent_id { - vec![ChannelEdge { - channel_id: channel.id, - parent_id, - }] - } else { - vec![] - }; + // let parent_edge = if let Some(parent_id) = parent_id { + // vec![ChannelEdge { + // channel_id: channel.id, + // parent_id, + // }] + // } else { + // vec![] + // }; this.update(&mut cx, |this, cx| { let task = this.update_channels( proto::UpdateChannels { channels: vec![channel], - insert_edge: parent_edge, ..Default::default() }, cx, @@ -515,53 +505,16 @@ impl ChannelStore { }) } - pub fn link_channel( - &mut self, - channel_id: ChannelId, - to: ChannelId, - cx: &mut ModelContext, - ) -> Task> { - let client = self.client.clone(); - cx.spawn(|_, _| async move { - let _ = client - .request(proto::LinkChannel { channel_id, to }) - .await?; - - Ok(()) - }) - } - - pub fn unlink_channel( - &mut self, - channel_id: ChannelId, - from: ChannelId, - cx: &mut ModelContext, - ) -> Task> { - let client = self.client.clone(); - cx.spawn(|_, _| async move { - let _ = client - .request(proto::UnlinkChannel { channel_id, from }) - .await?; - - Ok(()) - }) - } - pub fn move_channel( &mut self, channel_id: ChannelId, - from: ChannelId, to: ChannelId, cx: &mut ModelContext, ) -> Task> { let client = self.client.clone(); cx.spawn(|_, _| async move { let _ = client - .request(proto::MoveChannel { - channel_id, - from, - to, - }) + .request(proto::MoveChannel { channel_id, to }) .await?; Ok(()) @@ -956,6 +909,7 @@ impl ChannelStore { name: channel.name, unseen_note_version: None, unseen_message_id: None, + parent_path: channel.parent_path, }), ), } @@ -963,8 +917,6 @@ impl ChannelStore { let channels_changed = !payload.channels.is_empty() || !payload.delete_channels.is_empty() - || !payload.insert_edge.is_empty() - || !payload.delete_edge.is_empty() || !payload.unseen_channel_messages.is_empty() || !payload.unseen_channel_buffer_changes.is_empty(); @@ -1022,14 +974,6 @@ impl ChannelStore { unseen_channel_message.message_id, ); } - - for edge in payload.insert_edge { - index.insert_edge(edge.channel_id, edge.parent_id); - } - - for edge in payload.delete_edge { - index.delete_edge(edge.parent_id, edge.channel_id); - } } cx.notify(); @@ -1078,44 +1022,3 @@ impl ChannelStore { })) } } - -impl Deref for ChannelPath { - type Target = [ChannelId]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl ChannelPath { - pub fn new(path: Arc<[ChannelId]>) -> Self { - debug_assert!(path.len() >= 1); - Self(path) - } - - pub fn parent_id(&self) -> Option { - self.0.len().checked_sub(2).map(|i| self.0[i]) - } - - pub fn channel_id(&self) -> ChannelId { - self.0[self.0.len() - 1] - } -} - -impl From for Cow<'static, ChannelPath> { - fn from(value: ChannelPath) -> Self { - Cow::Owned(value) - } -} - -impl<'a> From<&'a ChannelPath> for Cow<'a, ChannelPath> { - fn from(value: &'a ChannelPath) -> Self { - Cow::Borrowed(value) - } -} - -impl Default for ChannelPath { - fn default() -> Self { - ChannelPath(Arc::from([])) - } -} diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index b221ce1b02..97b2ab6318 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -1,14 +1,11 @@ -use std::{ops::Deref, sync::Arc}; - use crate::{Channel, ChannelId}; use collections::BTreeMap; use rpc::proto; - -use super::ChannelPath; +use std::sync::Arc; #[derive(Default, Debug)] pub struct ChannelIndex { - paths: Vec, + channels_ordered: Vec, channels_by_id: BTreeMap>, } @@ -17,8 +14,12 @@ impl ChannelIndex { &self.channels_by_id } + pub fn ordered_channels(&self) -> &[ChannelId] { + &self.channels_ordered + } + pub fn clear(&mut self) { - self.paths.clear(); + self.channels_ordered.clear(); self.channels_by_id.clear(); } @@ -26,13 +27,13 @@ impl ChannelIndex { pub fn delete_channels(&mut self, channels: &[ChannelId]) { self.channels_by_id .retain(|channel_id, _| !channels.contains(channel_id)); - self.paths - .retain(|path| !path.iter().any(|channel_id| channels.contains(channel_id))); + self.channels_ordered + .retain(|channel_id| !channels.contains(channel_id)); } pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard { ChannelPathsInsertGuard { - paths: &mut self.paths, + channels_ordered: &mut self.channels_ordered, channels_by_id: &mut self.channels_by_id, } } @@ -75,42 +76,15 @@ impl ChannelIndex { } } -impl Deref for ChannelIndex { - type Target = [ChannelPath]; - - fn deref(&self) -> &Self::Target { - &self.paths - } -} - /// A guard for ensuring that the paths index maintains its sort and uniqueness /// invariants after a series of insertions #[derive(Debug)] pub struct ChannelPathsInsertGuard<'a> { - paths: &'a mut Vec, + channels_ordered: &'a mut Vec, channels_by_id: &'a mut BTreeMap>, } impl<'a> ChannelPathsInsertGuard<'a> { - /// Remove the given edge from this index. This will not remove the channel. - /// If this operation would result in a dangling edge, re-insert it. - pub fn delete_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { - self.paths.retain(|path| { - !path - .windows(2) - .any(|window| window == [parent_id, channel_id]) - }); - - // Ensure that there is at least one channel path in the index - if !self - .paths - .iter() - .any(|path| path.iter().any(|id| id == &channel_id)) - { - self.insert_root(channel_id); - } - } - pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) { insert_note_changed(&mut self.channels_by_id, channel_id, epoch, &version); } @@ -141,6 +115,7 @@ impl<'a> ChannelPathsInsertGuard<'a> { name: channel_proto.name, unseen_note_version: None, unseen_message_id: None, + parent_path: channel_proto.parent_path, }), ); self.insert_root(channel_proto.id); @@ -148,74 +123,35 @@ impl<'a> ChannelPathsInsertGuard<'a> { ret } - pub fn insert_edge(&mut self, channel_id: ChannelId, parent_id: ChannelId) { - let mut parents = Vec::new(); - let mut descendants = Vec::new(); - let mut ixs_to_remove = Vec::new(); - - for (ix, path) in self.paths.iter().enumerate() { - if path - .windows(2) - .any(|window| window[0] == parent_id && window[1] == channel_id) - { - // We already have this edge in the index - return; - } - if path.ends_with(&[parent_id]) { - parents.push(path); - } else if let Some(position) = path.iter().position(|id| id == &channel_id) { - if position == 0 { - ixs_to_remove.push(ix); - } - descendants.push(path.split_at(position).1); - } - } - - let mut new_paths = Vec::new(); - for parent in parents.iter() { - if descendants.is_empty() { - let mut new_path = Vec::with_capacity(parent.len() + 1); - new_path.extend_from_slice(parent); - new_path.push(channel_id); - new_paths.push(ChannelPath::new(new_path.into())); - } else { - for descendant in descendants.iter() { - let mut new_path = Vec::with_capacity(parent.len() + descendant.len()); - new_path.extend_from_slice(parent); - new_path.extend_from_slice(descendant); - new_paths.push(ChannelPath::new(new_path.into())); - } - } - } - - for ix in ixs_to_remove.into_iter().rev() { - self.paths.swap_remove(ix); - } - self.paths.extend(new_paths) - } - fn insert_root(&mut self, channel_id: ChannelId) { - self.paths.push(ChannelPath::new(Arc::from([channel_id]))); + self.channels_ordered.push(channel_id); } } impl<'a> Drop for ChannelPathsInsertGuard<'a> { fn drop(&mut self) { - self.paths.sort_by(|a, b| { - let a = channel_path_sorting_key(a, &self.channels_by_id); - let b = channel_path_sorting_key(b, &self.channels_by_id); + self.channels_ordered.sort_by(|a, b| { + let a = channel_path_sorting_key(*a, &self.channels_by_id); + let b = channel_path_sorting_key(*b, &self.channels_by_id); a.cmp(b) }); - self.paths.dedup(); + self.channels_ordered.dedup(); } } fn channel_path_sorting_key<'a>( - path: &'a [ChannelId], + id: ChannelId, channels_by_id: &'a BTreeMap>, -) -> impl 'a + Iterator> { - path.iter() - .map(|id| Some(channels_by_id.get(id)?.name.as_str())) +) -> impl Iterator { + let (parent_path, name) = channels_by_id + .get(&id) + .map_or((&[] as &[_], None), |channel| { + (channel.parent_path.as_slice(), Some(channel.name.as_str())) + }); + parent_path + .iter() + .filter_map(|id| Some(channels_by_id.get(id)?.name.as_str())) + .chain(name) } fn insert_note_changed( diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index dd4f24e2ca..43e0344b2c 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -20,12 +20,14 @@ fn test_update_channels(cx: &mut AppContext) { name: "b".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Admin.into(), + parent_path: Vec::new(), }, proto::Channel { id: 2, name: "a".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Member.into(), + parent_path: Vec::new(), }, ], ..Default::default() @@ -51,22 +53,14 @@ fn test_update_channels(cx: &mut AppContext) { name: "x".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Admin.into(), + parent_path: Vec::new(), }, proto::Channel { id: 4, name: "y".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Member.into(), - }, - ], - insert_edge: vec![ - proto::ChannelEdge { - parent_id: 1, - channel_id: 3, - }, - proto::ChannelEdge { - parent_id: 2, - channel_id: 4, + parent_path: Vec::new(), }, ], ..Default::default() @@ -98,28 +92,21 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { name: "a".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Admin.into(), + parent_path: Vec::new(), }, proto::Channel { id: 1, name: "b".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Admin.into(), + parent_path: Vec::new(), }, proto::Channel { id: 2, name: "c".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Admin.into(), - }, - ], - insert_edge: vec![ - proto::ChannelEdge { - parent_id: 0, - channel_id: 1, - }, - proto::ChannelEdge { - parent_id: 1, - channel_id: 2, + parent_path: Vec::new(), }, ], ..Default::default() @@ -170,6 +157,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { name: "the-channel".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Member.into(), + parent_path: vec![], }], ..Default::default() }); @@ -197,7 +185,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { // Join a channel and populate its existing messages. let channel = channel_store.update(cx, |store, cx| { - let channel_id = store.channel_dag_entries().next().unwrap().1.id; + let channel_id = store.ordered_channels().next().unwrap().1.id; store.open_channel_chat(channel_id, cx) }); let join_channel = server.receive::().await.unwrap(); @@ -384,7 +372,7 @@ fn assert_channels( ) { let actual = channel_store.read_with(cx, |store, _| { store - .channel_dag_entries() + .ordered_channels() .map(|(depth, channel)| (depth, channel.name.to_string(), channel.role)) .collect::>() }); diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 7fa808b498..775a4c1bbe 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -193,9 +193,12 @@ CREATE TABLE "channels" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" VARCHAR NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "visibility" VARCHAR NOT NULL + "visibility" VARCHAR NOT NULL, + "parent_path" TEXT ); +CREATE INDEX "index_channels_on_parent_path" ON "channels" ("parent_path"); + CREATE TABLE IF NOT EXISTS "channel_chat_participants" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "user_id" INTEGER NOT NULL REFERENCES users (id), @@ -224,12 +227,6 @@ CREATE TABLE "channel_message_mentions" ( PRIMARY KEY(message_id, start_offset) ); -CREATE TABLE "channel_paths" ( - "id_path" TEXT NOT NULL PRIMARY KEY, - "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE -); -CREATE INDEX "index_channel_paths_on_channel_id" ON "channel_paths" ("channel_id"); - CREATE TABLE "channel_members" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, diff --git a/crates/collab/migrations/20231024085546_move_channel_paths_to_channels_table.sql b/crates/collab/migrations/20231024085546_move_channel_paths_to_channels_table.sql new file mode 100644 index 0000000000..d9fc6c8722 --- /dev/null +++ b/crates/collab/migrations/20231024085546_move_channel_paths_to_channels_table.sql @@ -0,0 +1,12 @@ +ALTER TABLE channels ADD COLUMN parent_path TEXT; + +UPDATE channels +SET parent_path = substr( + channel_paths.id_path, + 2, + length(channel_paths.id_path) - length('/' || channel_paths.channel_id::text || '/') +) +FROM channel_paths +WHERE channel_paths.channel_id = channels.id; + +CREATE INDEX "index_channels_on_parent_path" ON "channels" ("parent_path"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index f4e2602cc6..df33416a46 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -13,7 +13,6 @@ use anyhow::anyhow; use collections::{BTreeMap, HashMap, HashSet}; use dashmap::DashMap; use futures::StreamExt; -use queries::channels::ChannelGraph; use rand::{prelude::StdRng, Rng, SeedableRng}; use rpc::{ proto::{self}, @@ -492,21 +491,33 @@ pub struct RemoveChannelMemberResult { pub notification_id: Option, } -#[derive(FromQueryResult, Debug, PartialEq, Eq, Hash)] +#[derive(Debug, PartialEq, Eq, Hash)] pub struct Channel { pub id: ChannelId, pub name: String, pub visibility: ChannelVisibility, pub role: ChannelRole, + pub parent_path: Vec, } impl Channel { + fn from_model(value: channel::Model, role: ChannelRole) -> Self { + Channel { + id: value.id, + visibility: value.visibility, + name: value.clone().name, + role, + parent_path: value.ancestors().collect(), + } + } + pub fn to_proto(&self) -> proto::Channel { proto::Channel { id: self.id.to_proto(), name: self.name.clone(), visibility: self.visibility.into(), role: self.role.into(), + parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(), } } } @@ -530,7 +541,7 @@ impl ChannelMember { #[derive(Debug, PartialEq)] pub struct ChannelsForUser { - pub channels: ChannelGraph, + pub channels: Vec, pub channel_participants: HashMap>, pub unseen_buffer_changes: Vec, pub channel_messages: Vec, diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 3aa9cff171..9eddb1f618 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -16,7 +16,8 @@ impl Database { connection: ConnectionId, ) -> Result { self.transaction(|tx| async move { - self.check_user_is_channel_participant(channel_id, user_id, &tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_participant(&channel, user_id, &tx) .await?; let buffer = channel::Model { @@ -129,9 +130,11 @@ impl Database { 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); + let channel = self + .get_channel_internal(ChannelId::from_proto(client_buffer.channel_id), &*tx) + .await?; if self - .check_user_is_channel_participant(channel_id, user_id, &*tx) + .check_user_is_channel_participant(&channel, user_id, &*tx) .await .is_err() { @@ -139,9 +142,9 @@ impl Database { continue; } - let buffer = self.get_channel_buffer(channel_id, &*tx).await?; + 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)) + .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel.id)) .all(&*tx) .await?; @@ -439,7 +442,8 @@ impl Database { Vec, )> { self.transaction(move |tx| async move { - self.check_user_is_channel_member(channel_id, user, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_member(&channel, user, &*tx) .await?; let buffer = buffer::Entity::find() @@ -482,7 +486,7 @@ impl Database { ) .await?; - channel_members = self.get_channel_participants(channel_id, &*tx).await?; + channel_members = self.get_channel_participants(&channel, &*tx).await?; let collaborators = self .get_channel_buffer_collaborators_internal(channel_id, &*tx) .await?; diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 9e7b1cabf5..b65e677764 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1,5 +1,6 @@ use super::*; -use rpc::proto::{channel_member::Kind, ChannelEdge}; +use rpc::proto::channel_member::Kind; +use sea_orm::TryGetableMany; impl Database { #[cfg(test)] @@ -42,55 +43,41 @@ impl Database { pub async fn create_channel( &self, name: &str, - parent: Option, + parent_channel_id: Option, admin_id: UserId, ) -> Result { let name = Self::sanitize_channel_name(name)?; self.transaction(move |tx| async move { - if let Some(parent) = parent { - self.check_user_is_channel_admin(parent, admin_id, &*tx) + let mut parent = None; + + if let Some(parent_channel_id) = parent_channel_id { + let parent_channel = self.get_channel_internal(parent_channel_id, &*tx).await?; + self.check_user_is_channel_admin(&parent_channel, admin_id, &*tx) .await?; + parent = Some(parent_channel); } let channel = channel::ActiveModel { id: ActiveValue::NotSet, name: ActiveValue::Set(name.to_string()), visibility: ActiveValue::Set(ChannelVisibility::Members), + parent_path: ActiveValue::Set( + parent + .as_ref() + .map_or(String::new(), |parent| parent.path()), + ), } .insert(&*tx) .await?; - if let Some(parent) = parent { - let sql = r#" - INSERT INTO channel_paths - (id_path, channel_id) - SELECT - id_path || $1 || '/', $2 - FROM - channel_paths - WHERE - channel_id = $3 - "#; - let channel_paths_stmt = Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - [ - channel.id.to_proto().into(), - channel.id.to_proto().into(), - parent.to_proto().into(), - ], - ); - tx.execute(channel_paths_stmt).await?; + let participants_to_update; + if let Some(parent) = &parent { + participants_to_update = self + .participants_to_notify_for_channel_change(parent, &*tx) + .await?; } else { - channel_path::Entity::insert(channel_path::ActiveModel { - channel_id: ActiveValue::Set(channel.id), - id_path: ActiveValue::Set(format!("/{}/", channel.id)), - }) - .exec(&*tx) - .await?; - } + participants_to_update = vec![]; - if parent.is_none() { channel_member::ActiveModel { id: ActiveValue::NotSet, channel_id: ActiveValue::Set(channel.id), @@ -100,22 +87,10 @@ impl Database { } .insert(&*tx) .await?; - } - - let participants_to_update = if let Some(parent) = parent { - self.participants_to_notify_for_channel_change(parent, &*tx) - .await? - } else { - vec![] }; Ok(CreateChannelResult { - channel: Channel { - id: channel.id, - visibility: channel.visibility, - name: channel.name, - role: ChannelRole::Admin, - }, + channel: Channel::from_model(channel, ChannelRole::Admin), participants_to_update, }) }) @@ -130,20 +105,14 @@ impl Database { environment: &str, ) -> Result<(JoinRoom, Option, ChannelRole)> { self.transaction(move |tx| async move { + let channel = self.get_channel_internal(channel_id, &*tx).await?; + let mut role = self.channel_role_for_user(&channel, user_id, &*tx).await?; + let mut accept_invite_result = None; - let channel = channel::Entity::find() - .filter(channel::Column::Id.eq(channel_id)) - .one(&*tx) - .await?; - - let mut role = self - .channel_role_for_user(channel_id, user_id, &*tx) - .await?; - - if role.is_none() && channel.is_some() { + if role.is_none() { if let Some(invitation) = self - .pending_invite_for_channel(channel_id, user_id, &*tx) + .pending_invite_for_channel(&channel, user_id, &*tx) .await? { // note, this may be a parent channel @@ -156,31 +125,28 @@ impl Database { .await?; accept_invite_result = Some( - self.calculate_membership_updated(channel_id, user_id, &*tx) + self.calculate_membership_updated(&channel, user_id, &*tx) .await?, ); debug_assert!( - self.channel_role_for_user(channel_id, user_id, &*tx) - .await? - == role + self.channel_role_for_user(&channel, user_id, &*tx).await? == role ); } } - if role.is_none() - && channel.as_ref().map(|c| c.visibility) == Some(ChannelVisibility::Public) - { + + if channel.visibility == ChannelVisibility::Public { role = Some(ChannelRole::Guest); - let channel_id_to_join = self - .public_path_to_channel(channel_id, &*tx) + let channel_to_join = self + .public_ancestors_including_self(&channel, &*tx) .await? .first() .cloned() - .unwrap_or(channel_id); + .unwrap_or(channel.clone()); channel_member::Entity::insert(channel_member::ActiveModel { id: ActiveValue::NotSet, - channel_id: ActiveValue::Set(channel_id_to_join), + channel_id: ActiveValue::Set(channel_to_join.id), user_id: ActiveValue::Set(user_id), accepted: ActiveValue::Set(true), role: ActiveValue::Set(ChannelRole::Guest), @@ -189,19 +155,15 @@ impl Database { .await?; accept_invite_result = Some( - self.calculate_membership_updated(channel_id, user_id, &*tx) + self.calculate_membership_updated(&channel_to_join, user_id, &*tx) .await?, ); - debug_assert!( - self.channel_role_for_user(channel_id, user_id, &*tx) - .await? - == role - ); + debug_assert!(self.channel_role_for_user(&channel, user_id, &*tx).await? == role); } - if channel.is_none() || role.is_none() || role == Some(ChannelRole::Banned) { - Err(anyhow!("no such channel, or not allowed"))? + if role.is_none() || role == Some(ChannelRole::Banned) { + Err(anyhow!("not allowed"))? } let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); @@ -209,7 +171,7 @@ impl Database { .get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx) .await?; - self.join_channel_room_internal(channel_id, room_id, user_id, connection, &*tx) + self.join_channel_room_internal(room_id, user_id, connection, &*tx) .await .map(|jr| (jr, accept_invite_result, role.unwrap())) }) @@ -223,23 +185,21 @@ impl Database { admin_id: UserId, ) -> Result { self.transaction(move |tx| async move { - self.check_user_is_channel_admin(channel_id, admin_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + + self.check_user_is_channel_admin(&channel, admin_id, &*tx) .await?; let previous_members = self - .get_channel_participant_details_internal(channel_id, &*tx) + .get_channel_participant_details_internal(&channel, &*tx) .await?; - channel::ActiveModel { - id: ActiveValue::Unchanged(channel_id), - visibility: ActiveValue::Set(visibility), - ..Default::default() - } - .update(&*tx) - .await?; + let mut model = channel.into_active_model(); + model.visibility = ActiveValue::Set(visibility); + let channel = model.update(&*tx).await?; let mut participants_to_update: HashMap = self - .participants_to_notify_for_channel_change(channel_id, &*tx) + .participants_to_notify_for_channel_change(&channel, &*tx) .await? .into_iter() .collect(); @@ -249,10 +209,10 @@ impl Database { match visibility { ChannelVisibility::Members => { let all_descendents: Vec = self - .get_channel_descendants(vec![channel_id], &*tx) + .get_channel_descendants_including_self(vec![channel_id], &*tx) .await? .into_iter() - .map(|edge| ChannelId::from_proto(edge.channel_id)) + .map(|channel| channel.id) .collect(); channels_to_remove = channel::Entity::find() @@ -268,6 +228,7 @@ impl Database { .collect(); channels_to_remove.push(channel_id); + for member in previous_members { if member.role.can_only_see_public_descendants() { participants_to_remove.insert(member.user_id); @@ -275,11 +236,9 @@ impl Database { } } ChannelVisibility::Public => { - if let Some(public_parent_id) = - self.public_parent_channel_id(channel_id, &*tx).await? - { + if let Some(public_parent) = self.public_parent_channel(&channel, &*tx).await? { let parent_updates = self - .participants_to_notify_for_channel_change(public_parent_id, &*tx) + .participants_to_notify_for_channel_change(&public_parent, &*tx) .await?; for (user_id, channels) in parent_updates { @@ -304,39 +263,12 @@ impl Database { user_id: UserId, ) -> Result<(Vec, Vec)> { self.transaction(move |tx| async move { - self.check_user_is_channel_admin(channel_id, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_admin(&channel, user_id, &*tx) .await?; - // Don't remove descendant channels that have additional parents. - let mut channels_to_remove: HashSet = HashSet::default(); - channels_to_remove.insert(channel_id); - - let graph = self.get_channel_descendants([channel_id], &*tx).await?; - for edge in graph.iter() { - channels_to_remove.insert(ChannelId::from_proto(edge.channel_id)); - } - - { - let mut channels_to_keep = channel_path::Entity::find() - .filter( - channel_path::Column::ChannelId - .is_in(channels_to_remove.iter().copied()) - .and( - channel_path::Column::IdPath - .not_like(&format!("%/{}/%", channel_id)), - ), - ) - .stream(&*tx) - .await?; - while let Some(row) = channels_to_keep.next().await { - let row = row?; - channels_to_remove.remove(&row.channel_id); - } - } - - let channel_ancestors = self.get_channel_ancestors(channel_id, &*tx).await?; let members_to_notify: Vec = channel_member::Entity::find() - .filter(channel_member::Column::ChannelId.is_in(channel_ancestors)) + .filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self())) .select_only() .column(channel_member::Column::UserId) .distinct() @@ -344,25 +276,19 @@ impl Database { .all(&*tx) .await?; + let channels_to_remove = self + .get_channel_descendants_including_self(vec![channel.id], &*tx) + .await? + .into_iter() + .map(|channel| channel.id) + .collect::>(); + channel::Entity::delete_many() .filter(channel::Column::Id.is_in(channels_to_remove.iter().copied())) .exec(&*tx) .await?; - // Delete any other paths that include this channel - let sql = r#" - DELETE FROM channel_paths - WHERE - id_path LIKE '%' || $1 || '%' - "#; - let channel_paths_stmt = Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - [channel_id.to_proto().into()], - ); - tx.execute(channel_paths_stmt).await?; - - Ok((channels_to_remove.into_iter().collect(), members_to_notify)) + Ok((channels_to_remove, members_to_notify)) }) .await } @@ -375,7 +301,8 @@ impl Database { role: ChannelRole, ) -> Result { self.transaction(move |tx| async move { - self.check_user_is_channel_admin(channel_id, inviter_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_admin(&channel, inviter_id, &*tx) .await?; channel_member::ActiveModel { @@ -388,17 +315,7 @@ impl Database { .insert(&*tx) .await?; - let channel = channel::Entity::find_by_id(channel_id) - .one(&*tx) - .await? - .unwrap(); - - let channel = Channel { - id: channel.id, - visibility: channel.visibility, - name: channel.name, - role, - }; + let channel = Channel::from_model(channel, role); let notifications = self .create_notification( @@ -440,40 +357,27 @@ impl Database { self.transaction(move |tx| async move { let new_name = Self::sanitize_channel_name(new_name)?.to_string(); + let channel = self.get_channel_internal(channel_id, &*tx).await?; let role = self - .check_user_is_channel_admin(channel_id, admin_id, &*tx) + .check_user_is_channel_admin(&channel, admin_id, &*tx) .await?; - let channel = channel::ActiveModel { - id: ActiveValue::Unchanged(channel_id), - name: ActiveValue::Set(new_name.clone()), - ..Default::default() - } - .update(&*tx) - .await?; + let mut model = channel.into_active_model(); + model.name = ActiveValue::Set(new_name.clone()); + let channel = model.update(&*tx).await?; let participants = self - .get_channel_participant_details_internal(channel_id, &*tx) + .get_channel_participant_details_internal(&channel, &*tx) .await?; Ok(RenameChannelResult { - channel: Channel { - id: channel.id, - name: channel.name, - visibility: channel.visibility, - role, - }, + channel: Channel::from_model(channel.clone(), role), participants_to_update: participants .iter() .map(|participant| { ( participant.user_id, - Channel { - id: channel.id, - name: new_name.clone(), - visibility: channel.visibility, - role: participant.role, - }, + Channel::from_model(channel.clone(), participant.role), ) }) .collect(), @@ -489,6 +393,8 @@ impl Database { accept: bool, ) -> Result { self.transaction(move |tx| async move { + let channel = self.get_channel_internal(channel_id, &*tx).await?; + let membership_update = if accept { let rows_affected = channel_member::Entity::update_many() .set(channel_member::ActiveModel { @@ -510,7 +416,7 @@ impl Database { } Some( - self.calculate_membership_updated(channel_id, user_id, &*tx) + self.calculate_membership_updated(&channel, user_id, &*tx) .await?, ) } else { @@ -554,53 +460,26 @@ impl Database { async fn calculate_membership_updated( &self, - channel_id: ChannelId, + channel: &channel::Model, user_id: UserId, tx: &DatabaseTransaction, ) -> Result { - let mut channel_to_refresh = channel_id; - let mut removed_channels: Vec = Vec::new(); - - // if the user was previously a guest of a parent public channel they may have seen this - // channel (or its descendants) in the tree already. - // Now they have new permissions, the graph of channels needs updating from that point. - if let Some(public_parent) = self.public_parent_channel_id(channel_id, &*tx).await? { - if self - .channel_role_for_user(public_parent, user_id, &*tx) - .await? - == Some(ChannelRole::Guest) - { - channel_to_refresh = public_parent; - } - } - - // remove all descendant channels from the user's tree - removed_channels.append( - &mut self - .get_channel_descendants(vec![channel_to_refresh], &*tx) - .await? - .into_iter() - .map(|edge| ChannelId::from_proto(edge.channel_id)) - .collect(), - ); - - let new_channels = self - .get_user_channels(user_id, Some(channel_to_refresh), &*tx) - .await?; - - // We only add the current channel to "moved" if the user has lost access, - // otherwise it would be made a root channel on the client. - if !new_channels - .channels - .channels - .iter() - .any(|c| c.id == channel_id) - { - removed_channels.push(channel_id); - } + let new_channels = self.get_user_channels(user_id, Some(channel), &*tx).await?; + let removed_channels = self + .get_channel_descendants_including_self(vec![channel.id], &*tx) + .await? + .into_iter() + .filter_map(|channel| { + if !new_channels.channels.iter().any(|c| c.id == channel.id) { + Some(channel.id) + } else { + None + } + }) + .collect::>(); Ok(MembershipUpdated { - channel_id, + channel_id: channel.id, new_channels, removed_channels, }) @@ -613,7 +492,8 @@ impl Database { admin_id: UserId, ) -> Result { self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, admin_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_admin(&channel, admin_id, &*tx) .await?; let result = channel_member::Entity::delete_many() @@ -631,7 +511,7 @@ impl Database { Ok(RemoveChannelMemberResult { membership_update: self - .calculate_membership_updated(channel_id, member_id, &*tx) + .calculate_membership_updated(&channel, member_id, &*tx) .await?, notification_id: self .remove_notification( @@ -673,11 +553,9 @@ impl Database { let channels = channels .into_iter() - .map(|channel| Channel { - id: channel.id, - name: channel.name, - visibility: channel.visibility, - role: role_for_channel[&channel.id], + .filter_map(|channel| { + let role = *role_for_channel.get(&channel.id)?; + Some(Channel::from_model(channel, role)) }) .collect(); @@ -698,15 +576,9 @@ impl Database { pub async fn get_user_channels( &self, user_id: UserId, - parent_channel_id: Option, + ancestor_channel: Option<&channel::Model>, tx: &DatabaseTransaction, ) -> Result { - // note: we could (maybe) win some efficiency here when parent_channel_id - // is set by getting just the role for that channel, then getting descendants - // with roles attached; but that's not as straightforward as it sounds - // because we need to calculate the path to the channel to make the query - // efficient, which currently requires an extra round trip to the database. - // Fix this later... let channel_memberships = channel_member::Entity::find() .filter( channel_member::Column::UserId @@ -716,117 +588,65 @@ impl Database { .all(&*tx) .await?; - let mut edges = self - .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) + let descendants = self + .get_channel_descendants_including_self( + channel_memberships.iter().map(|m| m.channel_id), + &*tx, + ) .await?; - let mut role_for_channel: HashMap = HashMap::default(); - + let mut roles_by_channel_id: HashMap = HashMap::default(); for membership in channel_memberships.iter() { - let included = - parent_channel_id.is_none() || membership.channel_id == parent_channel_id.unwrap(); - role_for_channel.insert(membership.channel_id, (membership.role, included)); + roles_by_channel_id.insert(membership.channel_id, membership.role); } - for ChannelEdge { - parent_id, - channel_id, - } in edges.iter() - { - let parent_id = ChannelId::from_proto(*parent_id); - let channel_id = ChannelId::from_proto(*channel_id); - debug_assert!(role_for_channel.get(&parent_id).is_some()); - let (parent_role, parent_included) = role_for_channel[&parent_id]; + let mut visible_channel_ids: HashSet = HashSet::default(); - if let Some((existing_role, included)) = role_for_channel.get(&channel_id) { - role_for_channel.insert( - channel_id, - (existing_role.max(parent_role), *included || parent_included), - ); - } else { - role_for_channel.insert( - channel_id, - ( - parent_role, - parent_included - || parent_channel_id.is_none() - || Some(channel_id) == parent_channel_id, - ), - ); - } - } + let channels: Vec = descendants + .into_iter() + .filter_map(|channel| { + let parent_role = channel + .parent_id() + .and_then(|parent_id| roles_by_channel_id.get(&parent_id)); - let mut channels: Vec = Vec::new(); - let mut channels_to_remove: HashSet = HashSet::default(); - - let mut rows = channel::Entity::find() - .filter(channel::Column::Id.is_in(role_for_channel.keys().copied())) - .stream(&*tx) - .await?; - - while let Some(row) = rows.next().await { - let channel = row?; - let (role, included) = role_for_channel[&channel.id]; - - if !included - || role == ChannelRole::Banned - || role == ChannelRole::Guest && channel.visibility != ChannelVisibility::Public - { - channels_to_remove.insert(channel.id.0 as u64); - continue; - } - - channels.push(Channel { - id: channel.id, - name: channel.name, - visibility: channel.visibility, - role, - }); - } - drop(rows); - - if !channels_to_remove.is_empty() { - // Note: this code assumes each channel has one parent. - // If there are multiple valid public paths to a channel, - // e.g. - // If both of these paths are present (* indicating public): - // - zed* -> projects -> vim* - // - zed* -> conrad -> public-projects* -> vim* - // Users would only see one of them (based on edge sort order) - let mut replacement_parent: HashMap = HashMap::default(); - for ChannelEdge { - parent_id, - channel_id, - } in edges.iter() - { - if channels_to_remove.contains(channel_id) { - replacement_parent.insert(*channel_id, *parent_id); - } - } - - let mut new_edges: Vec = Vec::new(); - 'outer: for ChannelEdge { - mut parent_id, - channel_id, - } in edges.iter() - { - if channels_to_remove.contains(channel_id) { - continue; - } - while channels_to_remove.contains(&parent_id) { - if let Some(new_parent_id) = replacement_parent.get(&parent_id) { - parent_id = *new_parent_id; + let role = if let Some(parent_role) = parent_role { + let role = if let Some(existing_role) = roles_by_channel_id.get(&channel.id) { + existing_role.max(*parent_role) } else { - continue 'outer; + *parent_role + }; + roles_by_channel_id.insert(channel.id, role); + role + } else { + *roles_by_channel_id.get(&channel.id)? + }; + + let can_see_parent_paths = role.can_see_all_descendants() + || role.can_only_see_public_descendants() + && channel.visibility == ChannelVisibility::Public; + if !can_see_parent_paths { + return None; + } + + visible_channel_ids.insert(channel.id); + + if let Some(ancestor) = ancestor_channel { + if !channel + .ancestors_including_self() + .any(|id| id == ancestor.id) + { + return None; } } - new_edges.push(ChannelEdge { - parent_id, - channel_id: *channel_id, - }) - } - edges = new_edges; - } + + let mut channel = Channel::from_model(channel, role); + channel + .parent_path + .retain(|id| visible_channel_ids.contains(&id)); + + Some(channel) + }) + .collect(); #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryUserIdsAndChannelIds { @@ -861,7 +681,7 @@ impl Database { .await?; Ok(ChannelsForUser { - channels: ChannelGraph { channels, edges }, + channels, channel_participants, unseen_buffer_changes: channel_buffer_changes, channel_messages: unseen_messages, @@ -870,7 +690,7 @@ impl Database { async fn participants_to_notify_for_channel_change( &self, - new_parent: ChannelId, + new_parent: &channel::Model, tx: &DatabaseTransaction, ) -> Result> { let mut results: Vec<(UserId, ChannelsForUser)> = Vec::new(); @@ -890,11 +710,10 @@ impl Database { )) } - let public_parent = self - .public_path_to_channel(new_parent, &*tx) - .await? - .last() - .copied(); + let public_parents = self + .public_ancestors_including_self(new_parent, &*tx) + .await?; + let public_parent = public_parents.last(); let Some(public_parent) = public_parent else { return Ok(results); @@ -931,7 +750,8 @@ impl Database { role: ChannelRole, ) -> Result { self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, admin_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_admin(&channel, admin_id, &*tx) .await?; let membership = channel_member::Entity::find() @@ -951,24 +771,16 @@ impl Database { update.role = ActiveValue::Set(role); let updated = channel_member::Entity::update(update).exec(&*tx).await?; - if !updated.accepted { - let channel = channel::Entity::find_by_id(channel_id) - .one(&*tx) - .await? - .unwrap(); - - return Ok(SetMemberRoleResult::InviteUpdated(Channel { - id: channel.id, - visibility: channel.visibility, - name: channel.name, - role, - })); + if updated.accepted { + Ok(SetMemberRoleResult::MembershipUpdated( + self.calculate_membership_updated(&channel, for_user, &*tx) + .await?, + )) + } else { + Ok(SetMemberRoleResult::InviteUpdated(Channel::from_model( + channel, role, + ))) } - - Ok(SetMemberRoleResult::MembershipUpdated( - self.calculate_membership_updated(channel_id, for_user, &*tx) - .await?, - )) }) .await } @@ -980,12 +792,13 @@ impl Database { ) -> Result> { let (role, members) = self .transaction(move |tx| async move { + let channel = self.get_channel_internal(channel_id, &*tx).await?; let role = self - .check_user_is_channel_participant(channel_id, user_id, &*tx) + .check_user_is_channel_participant(&channel, user_id, &*tx) .await?; Ok(( role, - self.get_channel_participant_details_internal(channel_id, &*tx) + self.get_channel_participant_details_internal(&channel, &*tx) .await?, )) }) @@ -1016,16 +829,9 @@ impl Database { async fn get_channel_participant_details_internal( &self, - channel_id: ChannelId, + channel: &channel::Model, tx: &DatabaseTransaction, ) -> Result> { - let channel_visibility = channel::Entity::find() - .filter(channel::Column::Id.eq(channel_id)) - .one(&*tx) - .await? - .map(|channel| channel.visibility) - .unwrap_or(ChannelVisibility::Members); - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryMemberDetails { UserId, @@ -1035,16 +841,14 @@ impl Database { Visibility, } - let tx = tx; - let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?; let mut stream = channel_member::Entity::find() .left_join(channel::Entity) - .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) + .filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self())) .select_only() .column(channel_member::Column::UserId) .column(channel_member::Column::Role) .column_as( - channel_member::Column::ChannelId.eq(channel_id), + channel_member::Column::ChannelId.eq(channel.id), QueryMemberDetails::IsDirectMember, ) .column(channel_member::Column::Accepted) @@ -1072,7 +876,7 @@ impl Database { if channel_role == ChannelRole::Guest && visibility != ChannelVisibility::Public - && channel_visibility != ChannelVisibility::Public + && channel.visibility != ChannelVisibility::Public { continue; } @@ -1108,11 +912,11 @@ impl Database { pub async fn get_channel_participants( &self, - channel_id: ChannelId, + channel: &channel::Model, tx: &DatabaseTransaction, ) -> Result> { let participants = self - .get_channel_participant_details_internal(channel_id, &*tx) + .get_channel_participant_details_internal(channel, &*tx) .await?; Ok(participants .into_iter() @@ -1122,11 +926,11 @@ impl Database { pub async fn check_user_is_channel_admin( &self, - channel_id: ChannelId, + channel: &channel::Model, user_id: UserId, tx: &DatabaseTransaction, ) -> Result { - let role = self.channel_role_for_user(channel_id, user_id, tx).await?; + let role = self.channel_role_for_user(channel, user_id, tx).await?; match role { Some(ChannelRole::Admin) => Ok(role.unwrap()), Some(ChannelRole::Member) @@ -1140,11 +944,11 @@ impl Database { pub async fn check_user_is_channel_member( &self, - channel_id: ChannelId, + channel: &channel::Model, user_id: UserId, tx: &DatabaseTransaction, ) -> Result { - let channel_role = self.channel_role_for_user(channel_id, user_id, tx).await?; + let channel_role = self.channel_role_for_user(channel, user_id, tx).await?; match channel_role { Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(channel_role.unwrap()), Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!( @@ -1155,11 +959,11 @@ impl Database { pub async fn check_user_is_channel_participant( &self, - channel_id: ChannelId, + channel: &channel::Model, user_id: UserId, tx: &DatabaseTransaction, ) -> Result { - let role = self.channel_role_for_user(channel_id, user_id, tx).await?; + let role = self.channel_role_for_user(channel, user_id, tx).await?; match role { Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => { Ok(role.unwrap()) @@ -1172,14 +976,12 @@ impl Database { pub async fn pending_invite_for_channel( &self, - channel_id: ChannelId, + channel: &channel::Model, user_id: UserId, tx: &DatabaseTransaction, ) -> Result> { - let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; - let row = channel_member::Entity::find() - .filter(channel_member::Column::ChannelId.is_in(channel_ids)) + .filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self())) .filter(channel_member::Column::UserId.eq(user_id)) .filter(channel_member::Column::Accepted.eq(false)) .one(&*tx) @@ -1188,88 +990,39 @@ impl Database { Ok(row) } - pub async fn parent_channel_id( + pub async fn public_parent_channel( &self, - channel_id: ChannelId, + channel: &channel::Model, tx: &DatabaseTransaction, - ) -> Result> { - let path = self.path_to_channel(channel_id, &*tx).await?; - if path.len() >= 2 { - Ok(Some(path[path.len() - 2])) - } else { - Ok(None) + ) -> Result> { + let mut path = self.public_ancestors_including_self(channel, &*tx).await?; + if path.last().unwrap().id == channel.id { + path.pop(); } + Ok(path.pop()) } - pub async fn public_parent_channel_id( + pub async fn public_ancestors_including_self( &self, - channel_id: ChannelId, + channel: &channel::Model, tx: &DatabaseTransaction, - ) -> Result> { - let path = self.public_path_to_channel(channel_id, &*tx).await?; - if path.len() >= 2 && path.last().copied() == Some(channel_id) { - Ok(Some(path[path.len() - 2])) - } else { - Ok(path.last().copied()) - } - } - - pub async fn path_to_channel( - &self, - channel_id: ChannelId, - tx: &DatabaseTransaction, - ) -> Result> { - let arbitary_path = channel_path::Entity::find() - .filter(channel_path::Column::ChannelId.eq(channel_id)) - .order_by(channel_path::Column::IdPath, sea_orm::Order::Desc) - .one(tx) - .await?; - - let Some(path) = arbitary_path else { - return Ok(vec![]); - }; - - Ok(path - .id_path - .trim_matches('/') - .split('/') - .map(|id| ChannelId::from_proto(id.parse().unwrap())) - .collect()) - } - - pub async fn public_path_to_channel( - &self, - channel_id: ChannelId, - tx: &DatabaseTransaction, - ) -> Result> { - let ancestor_ids = self.path_to_channel(channel_id, &*tx).await?; - - let rows = channel::Entity::find() - .filter(channel::Column::Id.is_in(ancestor_ids.iter().copied())) + ) -> Result> { + let visible_channels = channel::Entity::find() + .filter(channel::Column::Id.is_in(channel.ancestors_including_self())) .filter(channel::Column::Visibility.eq(ChannelVisibility::Public)) + .order_by_asc(channel::Column::ParentPath) .all(&*tx) .await?; - let mut visible_channels: HashSet = HashSet::default(); - - for row in rows { - visible_channels.insert(row.id); - } - - Ok(ancestor_ids - .into_iter() - .filter(|id| visible_channels.contains(id)) - .collect()) + Ok(visible_channels) } pub async fn channel_role_for_user( &self, - channel_id: ChannelId, + channel: &channel::Model, user_id: UserId, tx: &DatabaseTransaction, ) -> Result> { - let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryChannelMembership { ChannelId, @@ -1281,7 +1034,7 @@ impl Database { .left_join(channel::Entity) .filter( channel_member::Column::ChannelId - .is_in(channel_ids) + .is_in(channel.ancestors_including_self()) .and(channel_member::Column::UserId.eq(user_id)) .and(channel_member::Column::Accepted.eq(true)), ) @@ -1320,7 +1073,7 @@ impl Database { } ChannelRole::Guest => {} } - if channel_id == membership_channel { + if channel.id == membership_channel { current_channel_visibility = Some(visibility); } } @@ -1330,7 +1083,7 @@ impl Database { if is_participant && user_role.is_none() { if current_channel_visibility.is_none() { current_channel_visibility = channel::Entity::find() - .filter(channel::Column::Id.eq(channel_id)) + .filter(channel::Column::Id.eq(channel.id)) .one(&*tx) .await? .map(|channel| channel.visibility); @@ -1343,39 +1096,13 @@ impl Database { Ok(user_role) } - /// Returns the channel ancestors in arbitrary order - pub async fn get_channel_ancestors( - &self, - channel_id: ChannelId, - tx: &DatabaseTransaction, - ) -> Result> { - let paths = channel_path::Entity::find() - .filter(channel_path::Column::ChannelId.eq(channel_id)) - .order_by(channel_path::Column::IdPath, sea_orm::Order::Desc) - .all(tx) - .await?; - let mut channel_ids = Vec::new(); - for path in paths { - for id in path.id_path.trim_matches('/').split('/') { - if let Ok(id) = id.parse() { - let id = ChannelId::from_proto(id); - if let Err(ix) = channel_ids.binary_search(&id) { - channel_ids.insert(ix, id); - } - } - } - } - Ok(channel_ids) - } - - // Returns the channel desendants as a sorted list of edges for further processing. - // The edges are sorted such that you will see unknown channel ids as children - // before you see them as parents. - async fn get_channel_descendants( + // Get the descendants of the given set if channels, ordered by their + // path. + async fn get_channel_descendants_including_self( &self, channel_ids: impl IntoIterator, tx: &DatabaseTransaction, - ) -> Result> { + ) -> Result> { let mut values = String::new(); for id in channel_ids { if !values.is_empty() { @@ -1390,65 +1117,55 @@ impl Database { let sql = format!( r#" - SELECT - descendant_paths.* + SELECT DISTINCT + descendant_channels.*, + descendant_channels.parent_path || descendant_channels.id as full_path FROM - channel_paths parent_paths, channel_paths descendant_paths + channels parent_channels, channels descendant_channels WHERE - parent_paths.channel_id IN ({values}) AND - descendant_paths.id_path != parent_paths.id_path AND - descendant_paths.id_path LIKE (parent_paths.id_path || '%') + descendant_channels.id IN ({values}) OR + ( + parent_channels.id IN ({values}) AND + descendant_channels.parent_path LIKE (parent_channels.parent_path || parent_channels.id || '/%') + ) ORDER BY - descendant_paths.id_path - "# + full_path ASC + "# ); - let stmt = Statement::from_string(self.pool.get_database_backend(), sql); - - let mut paths = channel_path::Entity::find() - .from_raw_sql(stmt) - .stream(tx) - .await?; - - let mut results: Vec = Vec::new(); - while let Some(path) = paths.next().await { - let path = path?; - let ids: Vec<&str> = path.id_path.trim_matches('/').split('/').collect(); - - debug_assert!(ids.len() >= 2); - debug_assert!(ids[ids.len() - 1] == path.channel_id.to_string()); - - results.push(ChannelEdge { - parent_id: ids[ids.len() - 2].parse().unwrap(), - channel_id: ids[ids.len() - 1].parse().unwrap(), - }) - } - - Ok(results) + Ok(channel::Entity::find() + .from_raw_sql(Statement::from_string( + self.pool.get_database_backend(), + sql, + )) + .all(tx) + .await?) } /// Returns the channel with the given ID pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result { self.transaction(|tx| async move { + let channel = self.get_channel_internal(channel_id, &*tx).await?; let role = self - .check_user_is_channel_participant(channel_id, user_id, &*tx) + .check_user_is_channel_participant(&channel, user_id, &*tx) .await?; - let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?; - let Some(channel) = channel else { - Err(anyhow!("no such channel"))? - }; - - Ok(Channel { - id: channel.id, - visibility: channel.visibility, - role, - name: channel.name, - }) + Ok(Channel::from_model(channel, role)) }) .await } + pub async fn get_channel_internal( + &self, + channel_id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result { + Ok(channel::Entity::find_by_id(channel_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such channel"))?) + } + pub(crate) async fn get_or_create_channel_room( &self, channel_id: ChannelId, @@ -1484,203 +1201,66 @@ impl Database { Ok(room_id) } - // Insert an edge from the given channel to the given other channel. - pub async fn link_channel( - &self, - user: UserId, - channel: ChannelId, - to: ChannelId, - ) -> Result { - self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel, user, &*tx) - .await?; - - self.link_channel_internal(user, channel, to, &*tx).await - }) - .await - } - - pub async fn link_channel_internal( - &self, - user: UserId, - channel: ChannelId, - new_parent: ChannelId, - tx: &DatabaseTransaction, - ) -> Result { - self.check_user_is_channel_admin(new_parent, user, &*tx) - .await?; - - let paths = channel_path::Entity::find() - .filter(channel_path::Column::IdPath.like(&format!("%/{}/%", channel))) - .all(tx) - .await?; - - let mut new_path_suffixes = HashSet::default(); - for path in paths { - if let Some(start_offset) = path.id_path.find(&format!("/{}/", channel)) { - new_path_suffixes.insert(( - path.channel_id, - path.id_path[(start_offset + 1)..].to_string(), - )); - } - } - - let paths_to_new_parent = channel_path::Entity::find() - .filter(channel_path::Column::ChannelId.eq(new_parent)) - .all(tx) - .await?; - - let mut new_paths = Vec::new(); - for path in paths_to_new_parent { - if path.id_path.contains(&format!("/{}/", channel)) { - Err(anyhow!("cycle"))?; - } - - new_paths.extend(new_path_suffixes.iter().map(|(channel_id, path_suffix)| { - channel_path::ActiveModel { - channel_id: ActiveValue::Set(*channel_id), - id_path: ActiveValue::Set(format!("{}{}", &path.id_path, path_suffix)), - } - })); - } - - channel_path::Entity::insert_many(new_paths) - .exec(&*tx) - .await?; - - // remove any root edges for the channel we just linked - { - channel_path::Entity::delete_many() - .filter(channel_path::Column::IdPath.like(&format!("/{}/%", channel))) - .exec(&*tx) - .await?; - } - - let mut channel_info = self.get_user_channels(user, Some(channel), &*tx).await?; - - channel_info.channels.edges.push(ChannelEdge { - channel_id: channel.to_proto(), - parent_id: new_parent.to_proto(), - }); - - Ok(channel_info.channels) - } - - /// Unlink a channel from a given parent. This will add in a root edge if - /// the channel has no other parents after this operation. - pub async fn unlink_channel( - &self, - user: UserId, - channel: ChannelId, - from: ChannelId, - ) -> Result<()> { - self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel, user, &*tx) - .await?; - - self.unlink_channel_internal(user, channel, from, &*tx) - .await?; - - Ok(()) - }) - .await - } - - pub async fn unlink_channel_internal( - &self, - user: UserId, - channel: ChannelId, - from: ChannelId, - tx: &DatabaseTransaction, - ) -> Result<()> { - self.check_user_is_channel_admin(from, user, &*tx).await?; - - let sql = r#" - DELETE FROM channel_paths - WHERE - id_path LIKE '%/' || $1 || '/' || $2 || '/%' - RETURNING id_path, channel_id - "#; - - let paths = channel_path::Entity::find() - .from_raw_sql(Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - [from.to_proto().into(), channel.to_proto().into()], - )) - .all(&*tx) - .await?; - - let is_stranded = channel_path::Entity::find() - .filter(channel_path::Column::ChannelId.eq(channel)) - .count(&*tx) - .await? - == 0; - - // Make sure that there is always at least one path to the channel - if is_stranded { - let root_paths: Vec<_> = paths - .iter() - .map(|path| { - let start_offset = path.id_path.find(&format!("/{}/", channel)).unwrap(); - channel_path::ActiveModel { - channel_id: ActiveValue::Set(path.channel_id), - id_path: ActiveValue::Set(path.id_path[start_offset..].to_string()), - } - }) - .collect(); - - channel_path::Entity::insert_many(root_paths) - .exec(&*tx) - .await?; - } - - Ok(()) - } - /// Move a channel from one parent to another pub async fn move_channel( &self, channel_id: ChannelId, - old_parent_id: Option, new_parent_id: ChannelId, admin_id: UserId, ) -> Result> { + // check you're an admin of source and target (and maybe current channel) + // change parent_path on current channel + // change parent_path on all children + self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, admin_id, &*tx) + let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?; + let channel = self.get_channel_internal(channel_id, &*tx).await?; + + self.check_user_is_channel_admin(&channel, admin_id, &*tx) + .await?; + self.check_user_is_channel_admin(&new_parent, admin_id, &*tx) .await?; - debug_assert_eq!( - self.parent_channel_id(channel_id, &*tx).await?, - old_parent_id - ); + let previous_participants = self + .get_channel_participant_details_internal(&channel, &*tx) + .await?; - if old_parent_id == Some(new_parent_id) { + let old_path = format!("{}{}/", channel.parent_path, channel.id); + let new_parent_path = format!("{}{}/", new_parent.parent_path, new_parent_id); + let new_path = format!("{}{}/", new_parent_path, channel.id); + + if old_path == new_path { return Ok(None); } - let previous_participants = self - .get_channel_participant_details_internal(channel_id, &*tx) - .await?; - self.link_channel_internal(admin_id, channel_id, new_parent_id, &*tx) - .await?; + let mut channel = channel.into_active_model(); + channel.parent_path = ActiveValue::Set(new_parent_path); + channel.save(&*tx).await?; - if let Some(from) = old_parent_id { - self.unlink_channel_internal(admin_id, channel_id, from, &*tx) - .await?; - } + let descendent_ids = + ChannelId::find_by_statement::(Statement::from_sql_and_values( + self.pool.get_database_backend(), + " + UPDATE channels SET parent_path = REPLACE(parent_path, $1, $2) + WHERE parent_path LIKE $3 || '%' + RETURNING id + ", + [old_path.clone().into(), new_path.into(), old_path.into()], + )) + .all(&*tx) + .await?; let participants_to_update: HashMap = self - .participants_to_notify_for_channel_change(new_parent_id, &*tx) + .participants_to_notify_for_channel_change(&new_parent, &*tx) .await? .into_iter() .collect(); let mut moved_channels: HashSet = HashSet::default(); - moved_channels.insert(channel_id); - for edge in self.get_channel_descendants([channel_id], &*tx).await? { - moved_channels.insert(ChannelId::from_proto(edge.channel_id)); + for id in descendent_ids { + moved_channels.insert(id); } + moved_channels.insert(channel_id); let mut participants_to_remove: HashSet = HashSet::default(); for participant in previous_participants { @@ -1701,47 +1281,12 @@ impl Database { } } +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +enum QueryIds { + Id, +} + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryUserIds { UserId, } - -#[derive(Debug)] -pub struct ChannelGraph { - pub channels: Vec, - pub edges: Vec, -} - -impl ChannelGraph { - pub fn is_empty(&self) -> bool { - self.channels.is_empty() && self.edges.is_empty() - } -} - -#[cfg(test)] -impl PartialEq for ChannelGraph { - fn eq(&self, other: &Self) -> bool { - // Order independent comparison for tests - let channels_set = self.channels.iter().collect::>(); - let other_channels_set = other.channels.iter().collect::>(); - let edges_set = self - .edges - .iter() - .map(|edge| (edge.channel_id, edge.parent_id)) - .collect::>(); - let other_edges_set = other - .edges - .iter() - .map(|edge| (edge.channel_id, edge.parent_id)) - .collect::>(); - - channels_set == other_channels_set && edges_set == other_edges_set - } -} - -#[cfg(not(test))] -impl PartialEq for ChannelGraph { - fn eq(&self, other: &Self) -> bool { - self.channels == other.channels && self.edges == other.edges - } -} diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index 562c4e45c4..47bb27df39 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -1,5 +1,4 @@ use super::*; -use futures::Stream; use rpc::Notification; use sea_orm::TryInsertResult; use time::OffsetDateTime; @@ -12,7 +11,8 @@ impl Database { user_id: UserId, ) -> Result<()> { self.transaction(|tx| async move { - self.check_user_is_channel_participant(channel_id, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_participant(&channel, user_id, &*tx) .await?; channel_chat_participant::ActiveModel { id: ActiveValue::NotSet, @@ -80,7 +80,8 @@ impl Database { before_message_id: Option, ) -> Result> { self.transaction(|tx| async move { - self.check_user_is_channel_participant(channel_id, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_participant(&channel, user_id, &*tx) .await?; let mut condition = @@ -94,7 +95,7 @@ impl Database { .filter(condition) .order_by_desc(channel_message::Column::Id) .limit(count as u64) - .stream(&*tx) + .all(&*tx) .await?; self.load_channel_messages(rows, &*tx).await @@ -111,27 +112,23 @@ impl Database { let rows = channel_message::Entity::find() .filter(channel_message::Column::Id.is_in(message_ids.iter().copied())) .order_by_desc(channel_message::Column::Id) - .stream(&*tx) + .all(&*tx) .await?; - let mut channel_ids = HashSet::::default(); - let messages = self - .load_channel_messages( - rows.map(|row| { - row.map(|row| { - channel_ids.insert(row.channel_id); - row - }) - }), - &*tx, - ) - .await?; + let mut channels = HashMap::::default(); + for row in &rows { + channels.insert( + row.channel_id, + self.get_channel_internal(row.channel_id, &*tx).await?, + ); + } - for channel_id in channel_ids { - self.check_user_is_channel_member(channel_id, user_id, &*tx) + for (_, channel) in channels { + self.check_user_is_channel_participant(&channel, user_id, &*tx) .await?; } + let messages = self.load_channel_messages(rows, &*tx).await?; Ok(messages) }) .await @@ -139,26 +136,26 @@ impl Database { async fn load_channel_messages( &self, - mut rows: impl Send + Unpin + Stream>, + rows: Vec, tx: &DatabaseTransaction, ) -> Result> { - let mut messages = Vec::new(); - while let Some(row) = rows.next().await { - let row = row?; - let nonce = row.nonce.as_u64_pair(); - messages.push(proto::ChannelMessage { - id: row.id.to_proto(), - sender_id: row.sender_id.to_proto(), - body: row.body, - timestamp: row.sent_at.assume_utc().unix_timestamp() as u64, - mentions: vec![], - nonce: Some(proto::Nonce { - upper_half: nonce.0, - lower_half: nonce.1, - }), - }); - } - drop(rows); + let mut messages = rows + .into_iter() + .map(|row| { + let nonce = row.nonce.as_u64_pair(); + proto::ChannelMessage { + id: row.id.to_proto(), + sender_id: row.sender_id.to_proto(), + body: row.body, + timestamp: row.sent_at.assume_utc().unix_timestamp() as u64, + mentions: vec![], + nonce: Some(proto::Nonce { + upper_half: nonce.0, + lower_half: nonce.1, + }), + } + }) + .collect::>(); messages.reverse(); let mut mentions = channel_message_mention::Entity::find() @@ -203,7 +200,8 @@ impl Database { nonce: u128, ) -> Result { self.transaction(|tx| async move { - self.check_user_is_channel_participant(channel_id, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_participant(&channel, user_id, &*tx) .await?; let mut rows = channel_chat_participant::Entity::find() @@ -310,7 +308,7 @@ impl Database { } } - let mut channel_members = self.get_channel_participants(channel_id, &*tx).await?; + let mut channel_members = self.get_channel_participants(&channel, &*tx).await?; channel_members.retain(|member| !participant_user_ids.contains(member)); Ok(CreatedChannelMessage { @@ -483,8 +481,9 @@ impl Database { .await?; if result.rows_affected == 0 { + let channel = self.get_channel_internal(channel_id, &*tx).await?; if self - .check_user_is_channel_admin(channel_id, user_id, &*tx) + .check_user_is_channel_admin(&channel, user_id, &*tx) .await .is_ok() { diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 630d51cfe6..40fdf5d58f 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -50,10 +50,10 @@ impl Database { .map(|participant| participant.user_id), ); - let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; + let (channel, room) = self.get_channel_room(room_id, &tx).await?; let channel_members; - if let Some(channel_id) = channel_id { - channel_members = self.get_channel_participants(channel_id, &tx).await?; + if let Some(channel) = &channel { + channel_members = self.get_channel_participants(channel, &tx).await?; } else { channel_members = Vec::new(); @@ -69,7 +69,7 @@ impl Database { Ok(RefreshedRoom { room, - channel_id, + channel_id: channel.map(|channel| channel.id), channel_members, stale_participant_user_ids, canceled_calls_to_user_ids, @@ -381,7 +381,6 @@ impl Database { pub(crate) async fn join_channel_room_internal( &self, - channel_id: ChannelId, room_id: RoomId, user_id: UserId, connection: ConnectionId, @@ -420,11 +419,12 @@ impl Database { .exec(&*tx) .await?; - let room = self.get_room(room_id, &tx).await?; - let channel_members = self.get_channel_participants(channel_id, &tx).await?; + let (channel, room) = self.get_channel_room(room_id, &tx).await?; + let channel = channel.ok_or_else(|| anyhow!("no channel for room"))?; + let channel_members = self.get_channel_participants(&channel, &*tx).await?; Ok(JoinRoom { room, - channel_id: Some(channel_id), + channel_id: Some(channel.id), channel_members, }) } @@ -718,16 +718,16 @@ impl Database { }); } - let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; - let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_participants(channel_id, &tx).await? + let (channel, room) = self.get_channel_room(room_id, &tx).await?; + let channel_members = if let Some(channel) = &channel { + self.get_channel_participants(&channel, &tx).await? } else { Vec::new() }; Ok(RejoinedRoom { room, - channel_id, + channel_id: channel.map(|channel| channel.id), channel_members, rejoined_projects, reshared_projects, @@ -869,7 +869,7 @@ impl Database { .exec(&*tx) .await?; - let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; + let (channel, room) = self.get_channel_room(room_id, &tx).await?; let deleted = if room.participants.is_empty() { let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?; result.rows_affected > 0 @@ -877,14 +877,14 @@ impl Database { false }; - let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_participants(channel_id, &tx).await? + let channel_members = if let Some(channel) = &channel { + self.get_channel_participants(channel, &tx).await? } else { Vec::new() }; let left_room = LeftRoom { room, - channel_id, + channel_id: channel.map(|channel| channel.id), channel_members, left_projects, canceled_calls_to_user_ids, @@ -1072,7 +1072,7 @@ impl Database { &self, room_id: RoomId, tx: &DatabaseTransaction, - ) -> Result<(Option, proto::Room)> { + ) -> Result<(Option, proto::Room)> { let db_room = room::Entity::find_by_id(room_id) .one(tx) .await? @@ -1181,9 +1181,16 @@ impl Database { project_id: db_follower.project_id.to_proto(), }); } + drop(db_followers); + + let channel = if let Some(channel_id) = db_room.channel_id { + Some(self.get_channel_internal(channel_id, &*tx).await?) + } else { + None + }; Ok(( - db_room.channel_id, + channel, proto::Room { id: db_room.id.to_proto(), live_kit_room: db_room.live_kit_room, diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index 0acb266d9d..4f28ce4fbd 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -8,7 +8,6 @@ pub mod channel_chat_participant; pub mod channel_member; pub mod channel_message; pub mod channel_message_mention; -pub mod channel_path; pub mod contact; pub mod feature_flag; pub mod follower; diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index 0975a8cc30..e30ec9af61 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -8,6 +8,28 @@ pub struct Model { pub id: ChannelId, pub name: String, pub visibility: ChannelVisibility, + pub parent_path: String, +} + +impl Model { + pub fn parent_id(&self) -> Option { + self.ancestors().last() + } + + pub fn ancestors(&self) -> impl Iterator + '_ { + self.parent_path + .trim_end_matches('/') + .split('/') + .filter_map(|id| Some(ChannelId::from_proto(id.parse().ok()?))) + } + + pub fn ancestors_including_self(&self) -> impl Iterator + '_ { + self.ancestors().chain(Some(self.id)) + } + + pub fn path(&self) -> String { + format!("{}{}/", self.parent_path, self.id) + } } impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/channel_path.rs b/crates/collab/src/db/tables/channel_path.rs deleted file mode 100644 index 323f116dae..0000000000 --- a/crates/collab/src/db/tables/channel_path.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::db::ChannelId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "channel_paths")] -pub struct Model { - #[sea_orm(primary_key)] - pub id_path: String, - pub channel_id: ChannelId, -} - -impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 8f2939040d..b6a89ff6f8 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -7,7 +7,6 @@ mod message_tests; use super::*; use gpui::executor::Background; use parking_lot::Mutex; -use rpc::proto::ChannelEdge; use sea_orm::ConnectionTrait; use sqlx::migrate::MigrateDatabase; use std::sync::{ @@ -153,33 +152,17 @@ impl Drop for TestDb { } } -/// The second tuples are (channel_id, parent) -fn graph( - channels: &[(ChannelId, &'static str, ChannelRole)], - edges: &[(ChannelId, ChannelId)], -) -> ChannelGraph { - let mut graph = ChannelGraph { - channels: vec![], - edges: vec![], - }; - - for (id, name, role) in channels { - graph.channels.push(Channel { +fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str, ChannelRole)]) -> Vec { + channels + .iter() + .map(|(id, parent_path, name, role)| Channel { id: *id, name: name.to_string(), visibility: ChannelVisibility::Members, role: *role, + parent_path: parent_path.to_vec(), }) - } - - for (channel, parent) in edges { - graph.edges.push(ChannelEdge { - channel_id: channel.to_proto(), - parent_id: parent.to_proto(), - }) - } - - graph + .collect() } static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5); diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 32cba2fdd9..936765b8c9 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -1,18 +1,15 @@ -use std::sync::Arc; - use crate::{ db::{ - queries::channels::ChannelGraph, - tests::{graph, new_test_connection, new_test_user, TEST_RELEASE_CHANNEL}, - ChannelId, ChannelRole, Database, NewUserParams, RoomId, + tests::{channel_tree, new_test_connection, new_test_user, TEST_RELEASE_CHANNEL}, + Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId, }, test_both_dbs, }; -use collections::{HashMap, HashSet}; use rpc::{ proto::{self}, ConnectionId, }; +use std::sync::Arc; test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite); @@ -44,7 +41,10 @@ async fn test_channels(db: &Arc) { .unwrap(); let mut members = db - .transaction(|tx| async move { Ok(db.get_channel_participants(replace_id, &*tx).await?) }) + .transaction(|tx| async move { + let channel = db.get_channel_internal(replace_id, &*tx).await?; + Ok(db.get_channel_participants(&channel, &*tx).await?) + }) .await .unwrap(); members.sort(); @@ -61,42 +61,41 @@ async fn test_channels(db: &Arc) { let result = db.get_channels_for_user(a_id).await.unwrap(); assert_eq!( result.channels, - graph( - &[ - (zed_id, "zed", ChannelRole::Admin), - (crdb_id, "crdb", ChannelRole::Admin), - (livestreaming_id, "livestreaming", ChannelRole::Admin), - (replace_id, "replace", ChannelRole::Admin), - (rust_id, "rust", ChannelRole::Admin), - (cargo_id, "cargo", ChannelRole::Admin), - (cargo_ra_id, "cargo-ra", ChannelRole::Admin) - ], - &[ - (crdb_id, zed_id), - (livestreaming_id, zed_id), - (replace_id, zed_id), - (cargo_id, rust_id), - (cargo_ra_id, cargo_id), - ] - ) + channel_tree(&[ + (zed_id, &[], "zed", ChannelRole::Admin), + (crdb_id, &[zed_id], "crdb", ChannelRole::Admin), + ( + livestreaming_id, + &[zed_id], + "livestreaming", + ChannelRole::Admin + ), + (replace_id, &[zed_id], "replace", ChannelRole::Admin), + (rust_id, &[], "rust", ChannelRole::Admin), + (cargo_id, &[rust_id], "cargo", ChannelRole::Admin), + ( + cargo_ra_id, + &[rust_id, cargo_id], + "cargo-ra", + ChannelRole::Admin + ) + ],) ); let result = db.get_channels_for_user(b_id).await.unwrap(); assert_eq!( result.channels, - graph( - &[ - (zed_id, "zed", ChannelRole::Member), - (crdb_id, "crdb", ChannelRole::Member), - (livestreaming_id, "livestreaming", ChannelRole::Member), - (replace_id, "replace", ChannelRole::Member) - ], - &[ - (crdb_id, zed_id), - (livestreaming_id, zed_id), - (replace_id, zed_id) - ] - ) + channel_tree(&[ + (zed_id, &[], "zed", ChannelRole::Member), + (crdb_id, &[zed_id], "crdb", ChannelRole::Member), + ( + livestreaming_id, + &[zed_id], + "livestreaming", + ChannelRole::Member + ), + (replace_id, &[zed_id], "replace", ChannelRole::Member) + ],) ); // Update member permissions @@ -112,19 +111,17 @@ async fn test_channels(db: &Arc) { let result = db.get_channels_for_user(b_id).await.unwrap(); assert_eq!( result.channels, - graph( - &[ - (zed_id, "zed", ChannelRole::Admin), - (crdb_id, "crdb", ChannelRole::Admin), - (livestreaming_id, "livestreaming", ChannelRole::Admin), - (replace_id, "replace", ChannelRole::Admin) - ], - &[ - (crdb_id, zed_id), - (livestreaming_id, zed_id), - (replace_id, zed_id) - ] - ) + channel_tree(&[ + (zed_id, &[], "zed", ChannelRole::Admin), + (crdb_id, &[zed_id], "crdb", ChannelRole::Admin), + ( + livestreaming_id, + &[zed_id], + "livestreaming", + ChannelRole::Admin + ), + (replace_id, &[zed_id], "replace", ChannelRole::Admin) + ],) ); // Remove a single channel @@ -327,14 +324,10 @@ async fn test_channel_renames(db: &Arc) { .await .unwrap(); - let zed_archive_id = zed_id; - - let channel = db.get_channel(zed_archive_id, user_1).await.unwrap(); + let channel = db.get_channel(zed_id, user_1).await.unwrap(); assert_eq!(channel.name, "zed-archive"); - let non_permissioned_rename = db - .rename_channel(zed_archive_id, user_2, "hacked-lol") - .await; + let non_permissioned_rename = db.rename_channel(zed_id, user_2, "hacked-lol").await; assert!(non_permissioned_rename.is_err()); let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await; @@ -383,328 +376,16 @@ async fn test_db_channel_moving(db: &Arc) { // /- gpui2 // zed -- crdb - livestreaming - livestreaming_dag let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( + assert_channel_tree( result.channels, &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), + (zed_id, &[]), + (crdb_id, &[zed_id]), + (livestreaming_id, &[zed_id, crdb_id]), + (livestreaming_dag_id, &[zed_id, crdb_id, livestreaming_id]), + (gpui2_id, &[zed_id]), ], ); - - // Attempt to make a cycle - assert!(db - .link_channel(a_id, zed_id, livestreaming_id) - .await - .is_err()); - - // // ======================================================================== - // // Make a link - // db.link_channel(a_id, livestreaming_id, zed_id) - // .await - // .unwrap(); - - // // DAG is now: - // // /- gpui2 - // // zed -- crdb - livestreaming - livestreaming_dag - // // \---------/ - // let result = db.get_channels_for_user(a_id).await.unwrap(); - // assert_dag( - // result.channels, - // &[ - // (zed_id, None), - // (crdb_id, Some(zed_id)), - // (gpui2_id, Some(zed_id)), - // (livestreaming_id, Some(zed_id)), - // (livestreaming_id, Some(crdb_id)), - // (livestreaming_dag_id, Some(livestreaming_id)), - // ], - // ); - - // // ======================================================================== - // // Create a new channel below a channel with multiple parents - // let livestreaming_dag_sub_id = db - // .create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), a_id) - // .await - // .unwrap(); - - // // DAG is now: - // // /- gpui2 - // // zed -- crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id - // // \---------/ - // let result = db.get_channels_for_user(a_id).await.unwrap(); - // assert_dag( - // result.channels, - // &[ - // (zed_id, None), - // (crdb_id, Some(zed_id)), - // (gpui2_id, Some(zed_id)), - // (livestreaming_id, Some(zed_id)), - // (livestreaming_id, Some(crdb_id)), - // (livestreaming_dag_id, Some(livestreaming_id)), - // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - // ], - // ); - - // // ======================================================================== - // // Test a complex DAG by making another link - // let returned_channels = db - // .link_channel(a_id, livestreaming_dag_sub_id, livestreaming_id) - // .await - // .unwrap(); - - // // DAG is now: - // // /- gpui2 /---------------------\ - // // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id - // // \--------/ - - // // make sure we're getting just the new link - // // Not using the assert_dag helper because we want to make sure we're returning the full data - // pretty_assertions::assert_eq!( - // returned_channels, - // graph( - // &[( - // livestreaming_dag_sub_id, - // "livestreaming_dag_sub", - // ChannelRole::Admin - // )], - // &[(livestreaming_dag_sub_id, livestreaming_id)] - // ) - // ); - - // let result = db.get_channels_for_user(a_id).await.unwrap(); - // assert_dag( - // result.channels, - // &[ - // (zed_id, None), - // (crdb_id, Some(zed_id)), - // (gpui2_id, Some(zed_id)), - // (livestreaming_id, Some(zed_id)), - // (livestreaming_id, Some(crdb_id)), - // (livestreaming_dag_id, Some(livestreaming_id)), - // (livestreaming_dag_sub_id, Some(livestreaming_id)), - // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - // ], - // ); - - // // ======================================================================== - // // Test a complex DAG by making another link - // let returned_channels = db - // .link_channel(a_id, livestreaming_id, gpui2_id) - // .await - // .unwrap(); - - // // DAG is now: - // // /- gpui2 -\ /---------------------\ - // // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub_id - // // \---------/ - - // // Make sure that we're correctly getting the full sub-dag - // pretty_assertions::assert_eq!( - // returned_channels, - // graph( - // &[ - // (livestreaming_id, "livestreaming", ChannelRole::Admin), - // ( - // livestreaming_dag_id, - // "livestreaming_dag", - // ChannelRole::Admin - // ), - // ( - // livestreaming_dag_sub_id, - // "livestreaming_dag_sub", - // ChannelRole::Admin - // ), - // ], - // &[ - // (livestreaming_id, gpui2_id), - // (livestreaming_dag_id, livestreaming_id), - // (livestreaming_dag_sub_id, livestreaming_id), - // (livestreaming_dag_sub_id, livestreaming_dag_id), - // ] - // ) - // ); - - // let result = db.get_channels_for_user(a_id).await.unwrap(); - // assert_dag( - // result.channels, - // &[ - // (zed_id, None), - // (crdb_id, Some(zed_id)), - // (gpui2_id, Some(zed_id)), - // (livestreaming_id, Some(zed_id)), - // (livestreaming_id, Some(crdb_id)), - // (livestreaming_id, Some(gpui2_id)), - // (livestreaming_dag_id, Some(livestreaming_id)), - // (livestreaming_dag_sub_id, Some(livestreaming_id)), - // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - // ], - // ); - - // // ======================================================================== - // // Test unlinking in a complex DAG by removing the inner link - // db.unlink_channel(a_id, livestreaming_dag_sub_id, livestreaming_id) - // .await - // .unwrap(); - - // // DAG is now: - // // /- gpui2 -\ - // // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub - // // \---------/ - - // let result = db.get_channels_for_user(a_id).await.unwrap(); - // assert_dag( - // result.channels, - // &[ - // (zed_id, None), - // (crdb_id, Some(zed_id)), - // (gpui2_id, Some(zed_id)), - // (livestreaming_id, Some(gpui2_id)), - // (livestreaming_id, Some(zed_id)), - // (livestreaming_id, Some(crdb_id)), - // (livestreaming_dag_id, Some(livestreaming_id)), - // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - // ], - // ); - - // // ======================================================================== - // // Test unlinking in a complex DAG by removing the inner link - // db.unlink_channel(a_id, livestreaming_id, gpui2_id) - // .await - // .unwrap(); - - // // DAG is now: - // // /- gpui2 - // // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub - // // \---------/ - // let result = db.get_channels_for_user(a_id).await.unwrap(); - // assert_dag( - // result.channels, - // &[ - // (zed_id, None), - // (crdb_id, Some(zed_id)), - // (gpui2_id, Some(zed_id)), - // (livestreaming_id, Some(zed_id)), - // (livestreaming_id, Some(crdb_id)), - // (livestreaming_dag_id, Some(livestreaming_id)), - // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - // ], - // ); - - // // ======================================================================== - // // Test moving DAG nodes by moving livestreaming to be below gpui2 - // db.move_channel(livestreaming_id, Some(crdb_id), gpui2_id, a_id) - // .await - // .unwrap(); - - // // DAG is now: - // // /- gpui2 -- livestreaming - livestreaming_dag - livestreaming_dag_sub - // // zed - crdb / - // // \---------/ - // let result = db.get_channels_for_user(a_id).await.unwrap(); - // assert_dag( - // result.channels, - // &[ - // (zed_id, None), - // (crdb_id, Some(zed_id)), - // (gpui2_id, Some(zed_id)), - // (livestreaming_id, Some(zed_id)), - // (livestreaming_id, Some(gpui2_id)), - // (livestreaming_dag_id, Some(livestreaming_id)), - // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - // ], - // ); - - // // ======================================================================== - // // Deleting a channel should not delete children that still have other parents - // db.delete_channel(gpui2_id, a_id).await.unwrap(); - - // // DAG is now: - // // zed - crdb - // // \- livestreaming - livestreaming_dag - livestreaming_dag_sub - // let result = db.get_channels_for_user(a_id).await.unwrap(); - // assert_dag( - // result.channels, - // &[ - // (zed_id, None), - // (crdb_id, Some(zed_id)), - // (livestreaming_id, Some(zed_id)), - // (livestreaming_dag_id, Some(livestreaming_id)), - // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - // ], - // ); - - // // ======================================================================== - // // Unlinking a channel from it's parent should automatically promote it to a root channel - // db.unlink_channel(a_id, crdb_id, zed_id).await.unwrap(); - - // // DAG is now: - // // crdb - // // zed - // // \- livestreaming - livestreaming_dag - livestreaming_dag_sub - - // let result = db.get_channels_for_user(a_id).await.unwrap(); - // assert_dag( - // result.channels, - // &[ - // (zed_id, None), - // (crdb_id, None), - // (livestreaming_id, Some(zed_id)), - // (livestreaming_dag_id, Some(livestreaming_id)), - // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - // ], - // ); - - // // ======================================================================== - // // You should be able to move a root channel into a non-root channel - // db.link_channel(a_id, crdb_id, zed_id).await.unwrap(); - - // // DAG is now: - // // zed - crdb - // // \- livestreaming - livestreaming_dag - livestreaming_dag_sub - - // let result = db.get_channels_for_user(a_id).await.unwrap(); - // assert_dag( - // result.channels, - // &[ - // (zed_id, None), - // (crdb_id, Some(zed_id)), - // (livestreaming_id, Some(zed_id)), - // (livestreaming_dag_id, Some(livestreaming_id)), - // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - // ], - // ); - - // // ======================================================================== - // // Prep for DAG deletion test - // db.link_channel(a_id, livestreaming_id, crdb_id) - // .await - // .unwrap(); - - // // DAG is now: - // // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub - // // \--------/ - - // let result = db.get_channels_for_user(a_id).await.unwrap(); - // assert_dag( - // result.channels, - // &[ - // (zed_id, None), - // (crdb_id, Some(zed_id)), - // (livestreaming_id, Some(zed_id)), - // (livestreaming_id, Some(crdb_id)), - // (livestreaming_dag_id, Some(livestreaming_id)), - // (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - // ], - // ); - - // // Deleting the parent of a DAG should delete the whole DAG: - // db.delete_channel(zed_id, a_id).await.unwrap(); - // let result = db.get_channels_for_user(a_id).await.unwrap(); - - // assert!(result.channels.is_empty()) } test_both_dbs!( @@ -743,37 +424,20 @@ async fn test_db_channel_moving_bugs(db: &Arc) { // Move to same parent should be a no-op assert!(db - .move_channel(projects_id, Some(zed_id), zed_id, user_id) + .move_channel(projects_id, zed_id, user_id) .await .unwrap() .is_none()); let result = db.get_channels_for_user(user_id).await.unwrap(); - assert_dag( + assert_channel_tree( result.channels, &[ - (zed_id, None), - (projects_id, Some(zed_id)), - (livestreaming_id, Some(projects_id)), + (zed_id, &[]), + (projects_id, &[zed_id]), + (livestreaming_id, &[zed_id, projects_id]), ], ); - - // Stranding a channel should retain it's sub channels - // Commented out as we don't fix permissions when this happens yet. - // - // db.unlink_channel(user_id, projects_id, zed_id) - // .await - // .unwrap(); - - // let result = db.get_channels_for_user(user_id).await.unwrap(); - // assert_dag( - // result.channels, - // &[ - // (zed_id, None), - // (projects_id, None), - // (livestreaming_id, Some(projects_id)), - // ], - // ); } test_both_dbs!( @@ -788,44 +452,52 @@ async fn test_user_is_channel_participant(db: &Arc) { let guest = new_test_user(db, "guest@example.com").await; let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); - let active_channel = db + let active_channel_id = db .create_sub_channel("active", zed_channel, admin) .await .unwrap(); - let vim_channel = db - .create_sub_channel("vim", active_channel, admin) + let vim_channel_id = db + .create_sub_channel("vim", active_channel_id, admin) .await .unwrap(); - db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin) + db.set_channel_visibility(vim_channel_id, crate::db::ChannelVisibility::Public, admin) .await .unwrap(); - db.invite_channel_member(active_channel, member, admin, ChannelRole::Member) + db.invite_channel_member(active_channel_id, member, admin, ChannelRole::Member) .await .unwrap(); - db.invite_channel_member(vim_channel, guest, admin, ChannelRole::Guest) + db.invite_channel_member(vim_channel_id, guest, admin, ChannelRole::Guest) .await .unwrap(); - db.respond_to_channel_invite(active_channel, member, true) + db.respond_to_channel_invite(active_channel_id, member, true) .await .unwrap(); db.transaction(|tx| async move { - db.check_user_is_channel_participant(vim_channel, admin, &*tx) - .await + db.check_user_is_channel_participant( + &db.get_channel_internal(vim_channel_id, &*tx).await?, + admin, + &*tx, + ) + .await }) .await .unwrap(); db.transaction(|tx| async move { - db.check_user_is_channel_participant(vim_channel, member, &*tx) - .await + db.check_user_is_channel_participant( + &db.get_channel_internal(vim_channel_id, &*tx).await?, + member, + &*tx, + ) + .await }) .await .unwrap(); let mut members = db - .get_channel_participant_details(vim_channel, admin) + .get_channel_participant_details(vim_channel_id, admin) .await .unwrap(); @@ -852,38 +524,49 @@ async fn test_user_is_channel_participant(db: &Arc) { ] ); - db.respond_to_channel_invite(vim_channel, guest, true) + db.respond_to_channel_invite(vim_channel_id, guest, true) .await .unwrap(); db.transaction(|tx| async move { - db.check_user_is_channel_participant(vim_channel, guest, &*tx) - .await + db.check_user_is_channel_participant( + &db.get_channel_internal(vim_channel_id, &*tx).await?, + guest, + &*tx, + ) + .await }) .await .unwrap(); let channels = db.get_channels_for_user(guest).await.unwrap().channels; - assert_dag(channels, &[(vim_channel, None)]); + assert_channel_tree(channels, &[(vim_channel_id, &[])]); let channels = db.get_channels_for_user(member).await.unwrap().channels; - assert_dag( + assert_channel_tree( channels, - &[(active_channel, None), (vim_channel, Some(active_channel))], + &[ + (active_channel_id, &[]), + (vim_channel_id, &[active_channel_id]), + ], ); - db.set_channel_member_role(vim_channel, admin, guest, ChannelRole::Banned) + db.set_channel_member_role(vim_channel_id, admin, guest, ChannelRole::Banned) .await .unwrap(); assert!(db .transaction(|tx| async move { - db.check_user_is_channel_participant(vim_channel, guest, &*tx) - .await + db.check_user_is_channel_participant( + &db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(), + guest, + &*tx, + ) + .await }) .await .is_err()); let mut members = db - .get_channel_participant_details(vim_channel, admin) + .get_channel_participant_details(vim_channel_id, admin) .await .unwrap(); @@ -910,7 +593,7 @@ async fn test_user_is_channel_participant(db: &Arc) { ] ); - db.remove_channel_member(vim_channel, guest, admin) + db.remove_channel_member(vim_channel_id, guest, admin) .await .unwrap(); @@ -924,7 +607,7 @@ async fn test_user_is_channel_participant(db: &Arc) { // currently people invited to parent channels are not shown here let mut members = db - .get_channel_participant_details(vim_channel, admin) + .get_channel_participant_details(vim_channel_id, admin) .await .unwrap(); @@ -951,28 +634,42 @@ async fn test_user_is_channel_participant(db: &Arc) { .unwrap(); db.transaction(|tx| async move { - db.check_user_is_channel_participant(zed_channel, guest, &*tx) - .await + db.check_user_is_channel_participant( + &db.get_channel_internal(zed_channel, &*tx).await.unwrap(), + guest, + &*tx, + ) + .await }) .await .unwrap(); assert!(db .transaction(|tx| async move { - db.check_user_is_channel_participant(active_channel, guest, &*tx) - .await + db.check_user_is_channel_participant( + &db.get_channel_internal(active_channel_id, &*tx) + .await + .unwrap(), + guest, + &*tx, + ) + .await }) .await .is_err(),); db.transaction(|tx| async move { - db.check_user_is_channel_participant(vim_channel, guest, &*tx) - .await + db.check_user_is_channel_participant( + &db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(), + guest, + &*tx, + ) + .await }) .await .unwrap(); let mut members = db - .get_channel_participant_details(vim_channel, admin) + .get_channel_participant_details(vim_channel_id, admin) .await .unwrap(); @@ -1000,9 +697,9 @@ async fn test_user_is_channel_participant(db: &Arc) { ); let channels = db.get_channels_for_user(guest).await.unwrap().channels; - assert_dag( + assert_channel_tree( channels, - &[(zed_channel, None), (vim_channel, Some(zed_channel))], + &[(zed_channel, &[]), (vim_channel_id, &[zed_channel])], ) } @@ -1047,15 +744,20 @@ async fn test_user_joins_correct_channel(db: &Arc) { let most_public = db .transaction(|tx| async move { Ok(db - .public_path_to_channel(vim_channel, &tx) + .public_ancestors_including_self( + &db.get_channel_internal(vim_channel, &*tx).await.unwrap(), + &tx, + ) .await? .first() .cloned()) }) .await - .unwrap(); + .unwrap() + .unwrap() + .id; - assert_eq!(most_public, Some(zed_channel)) + assert_eq!(most_public, zed_channel) } test_both_dbs!( @@ -1092,26 +794,14 @@ async fn test_guest_access(db: &Arc) { } #[track_caller] -fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option)]) { - let mut actual_map: HashMap> = HashMap::default(); - for channel in actual.channels { - actual_map.insert(channel.id, HashSet::default()); - } - for edge in actual.edges { - actual_map - .get_mut(&ChannelId::from_proto(edge.channel_id)) - .unwrap() - .insert(ChannelId::from_proto(edge.parent_id)); - } - - let mut expected_map: HashMap> = HashMap::default(); - - for (child, parent) in expected { - let entry = expected_map.entry(*child).or_default(); - if let Some(parent) = parent { - entry.insert(*parent); - } - } - - pretty_assertions::assert_eq!(actual_map, expected_map) +fn assert_channel_tree(actual: Vec, expected: &[(ChannelId, &[ChannelId])]) { + let actual = actual + .iter() + .map(|channel| (channel.id, channel.parent_path.as_slice())) + .collect::>(); + pretty_assertions::assert_eq!( + actual, + expected.to_vec(), + "wrong channel ids and parent paths" + ); } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index dda638e107..01e8530e67 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -277,8 +277,6 @@ impl Server { .add_request_handler(get_channel_messages_by_id) .add_request_handler(get_notifications) .add_request_handler(mark_notification_as_read) - .add_request_handler(link_channel) - .add_request_handler(unlink_channel) .add_request_handler(move_channel) .add_request_handler(follow) .add_message_handler(unfollow) @@ -2472,52 +2470,19 @@ async fn rename_channel( Ok(()) } -// TODO: Implement in terms of symlinks -// Current behavior of this is more like 'Move root channel' -async fn link_channel( - request: proto::LinkChannel, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - let channel_id = ChannelId::from_proto(request.channel_id); - let to = ChannelId::from_proto(request.to); - - let result = db - .move_channel(channel_id, None, to, session.user_id) - .await?; - drop(db); - - notify_channel_moved(result, session).await?; - - response.send(Ack {})?; - - Ok(()) -} - -// TODO: Implement in terms of symlinks -async fn unlink_channel( - _request: proto::UnlinkChannel, - _response: Response, - _session: Session, -) -> Result<()> { - Err(anyhow!("unimplemented").into()) -} - async fn move_channel( request: proto::MoveChannel, response: Response, session: Session, ) -> Result<()> { - let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let from_parent = ChannelId::from_proto(request.from); let to = ChannelId::from_proto(request.to); - let result = db - .move_channel(channel_id, Some(from_parent), to, session.user_id) + let result = session + .db() + .await + .move_channel(channel_id, to, session.user_id) .await?; - drop(db); notify_channel_moved(result, session).await?; @@ -3244,18 +3209,12 @@ fn build_channels_update( ) -> proto::UpdateChannels { let mut update = proto::UpdateChannels::default(); - for channel in channels.channels.channels { - update.channels.push(proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - visibility: channel.visibility.into(), - role: channel.role.into(), - }); + for channel in channels.channels { + update.channels.push(channel.to_proto()); } update.unseen_channel_buffer_changes = channels.unseen_buffer_changes; update.unseen_channel_messages = channels.channel_messages; - update.insert_edge = channels.channels.edges; for (channel_id, participants) in channels.channel_participants { update @@ -3267,12 +3226,7 @@ fn build_channels_update( } for channel in channel_invites { - update.channel_invitations.push(proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - visibility: channel.visibility.into(), - role: channel.role.into(), - }); + update.channel_invitations.push(channel.to_proto()); } update diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 931610f5ff..5ca40a3c2d 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -3,7 +3,7 @@ use crate::{ tests::TestServer, }; use call::ActiveCall; -use channel::{Channel, ACKNOWLEDGE_DEBOUNCE_INTERVAL}; +use channel::ACKNOWLEDGE_DEBOUNCE_INTERVAL; use client::ParticipantIndex; use client::{Collaborator, UserId}; use collab_ui::channel_view::ChannelView; @@ -11,10 +11,7 @@ use collections::HashMap; use editor::{Anchor, Editor, ToOffset}; use futures::future; use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext}; -use rpc::{ - proto::{self, PeerId}, - RECEIVE_TIMEOUT, -}; +use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; use serde_json::json; use std::{ops::Range, sync::Arc}; @@ -411,10 +408,7 @@ async fn test_channel_buffer_disconnect( deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); channel_buffer_a.update(cx_a, |buffer, cx| { - assert_eq!( - buffer.channel(cx).unwrap().as_ref(), - &channel(channel_id, "the-channel", proto::ChannelRole::Admin) - ); + assert_eq!(buffer.channel(cx).unwrap().name, "the-channel"); assert!(!buffer.is_connected()); }); @@ -441,17 +435,6 @@ async fn test_channel_buffer_disconnect( }); } -fn channel(id: u64, name: &'static str, role: proto::ChannelRole) -> Channel { - Channel { - id, - role, - name: name.to_string(), - visibility: proto::ChannelVisibility::Members, - unseen_note_version: None, - unseen_message_id: None, - } -} - #[gpui::test] async fn test_rejoin_channel_buffer( deterministic: Arc, diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 5b0bf02f8f..d2c5e1cec3 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -61,10 +61,7 @@ async fn test_core_channels( ); client_b.channel_store().read_with(cx_b, |channels, _| { - assert!(channels - .channel_dag_entries() - .collect::>() - .is_empty()) + assert!(channels.ordered_channels().collect::>().is_empty()) }); // Invite client B to channel A as client A. @@ -1019,7 +1016,7 @@ async fn test_channel_link_notifications( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.link_channel(vim_channel, active_channel, cx) + channel_store.move_channel(vim_channel, active_channel, cx) }) .await .unwrap(); @@ -1054,7 +1051,7 @@ async fn test_channel_link_notifications( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.link_channel(helix_channel, vim_channel, cx) + channel_store.move_channel(helix_channel, vim_channel, cx) }) .await .unwrap(); @@ -1427,7 +1424,7 @@ async fn test_channel_moving( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.move_channel(channel_d_id, channel_c_id, channel_b_id, cx) + channel_store.move_channel(channel_d_id, channel_b_id, cx) }) .await .unwrap(); @@ -1445,189 +1442,6 @@ async fn test_channel_moving( (channel_d_id, 2), ], ); - - // TODO: Restore this test once we have a way to make channel symlinks - // client_a - // .channel_store() - // .update(cx_a, |channel_store, cx| { - // channel_store.link_channel(channel_d_id, channel_c_id, cx) - // }) - // .await - // .unwrap(); - - // // Current shape for A: - // // /------\ - // // a - b -- c -- d - // assert_channels_list_shape( - // client_a.channel_store(), - // cx_a, - // &[ - // (channel_a_id, 0), - // (channel_b_id, 1), - // (channel_c_id, 2), - // (channel_d_id, 3), - // (channel_d_id, 2), - // ], - // ); - // - // let b_channels = server - // .make_channel_tree( - // &[ - // ("channel-mu", None), - // ("channel-gamma", Some("channel-mu")), - // ("channel-epsilon", Some("channel-mu")), - // ], - // (&client_b, cx_b), - // ) - // .await; - // let channel_mu_id = b_channels[0]; - // let channel_ga_id = b_channels[1]; - // let channel_ep_id = b_channels[2]; - - // // Current shape for B: - // // /- ep - // // mu -- ga - // assert_channels_list_shape( - // client_b.channel_store(), - // cx_b, - // &[(channel_mu_id, 0), (channel_ep_id, 1), (channel_ga_id, 1)], - // ); - - // client_a - // .add_admin_to_channel((&client_b, cx_b), channel_b_id, cx_a) - // .await; - - // // Current shape for B: - // // /- ep - // // mu -- ga - // // /---------\ - // // b -- c -- d - // assert_channels_list_shape( - // client_b.channel_store(), - // cx_b, - // &[ - // // New channels from a - // (channel_b_id, 0), - // (channel_c_id, 1), - // (channel_d_id, 2), - // (channel_d_id, 1), - // // B's old channels - // (channel_mu_id, 0), - // (channel_ep_id, 1), - // (channel_ga_id, 1), - // ], - // ); - - // client_b - // .add_admin_to_channel((&client_c, cx_c), channel_ep_id, cx_b) - // .await; - - // // Current shape for C: - // // - ep - // assert_channels_list_shape(client_c.channel_store(), cx_c, &[(channel_ep_id, 0)]); - - // client_b - // .channel_store() - // .update(cx_b, |channel_store, cx| { - // channel_store.link_channel(channel_b_id, channel_ep_id, cx) - // }) - // .await - // .unwrap(); - - // // Current shape for B: - // // /---------\ - // // /- ep -- b -- c -- d - // // mu -- ga - // assert_channels_list_shape( - // client_b.channel_store(), - // cx_b, - // &[ - // (channel_mu_id, 0), - // (channel_ep_id, 1), - // (channel_b_id, 2), - // (channel_c_id, 3), - // (channel_d_id, 4), - // (channel_d_id, 3), - // (channel_ga_id, 1), - // ], - // ); - - // // Current shape for C: - // // /---------\ - // // ep -- b -- c -- d - // assert_channels_list_shape( - // client_c.channel_store(), - // cx_c, - // &[ - // (channel_ep_id, 0), - // (channel_b_id, 1), - // (channel_c_id, 2), - // (channel_d_id, 3), - // (channel_d_id, 2), - // ], - // ); - - // client_b - // .channel_store() - // .update(cx_b, |channel_store, cx| { - // channel_store.link_channel(channel_ga_id, channel_b_id, cx) - // }) - // .await - // .unwrap(); - - // // Current shape for B: - // // /---------\ - // // /- ep -- b -- c -- d - // // / \ - // // mu ---------- ga - // assert_channels_list_shape( - // client_b.channel_store(), - // cx_b, - // &[ - // (channel_mu_id, 0), - // (channel_ep_id, 1), - // (channel_b_id, 2), - // (channel_c_id, 3), - // (channel_d_id, 4), - // (channel_d_id, 3), - // (channel_ga_id, 3), - // (channel_ga_id, 1), - // ], - // ); - - // // Current shape for A: - // // /------\ - // // a - b -- c -- d - // // \-- ga - // assert_channels_list_shape( - // client_a.channel_store(), - // cx_a, - // &[ - // (channel_a_id, 0), - // (channel_b_id, 1), - // (channel_c_id, 2), - // (channel_d_id, 3), - // (channel_d_id, 2), - // (channel_ga_id, 2), - // ], - // ); - - // // Current shape for C: - // // /-------\ - // // ep -- b -- c -- d - // // \-- ga - // assert_channels_list_shape( - // client_c.channel_store(), - // cx_c, - // &[ - // (channel_ep_id, 0), - // (channel_b_id, 1), - // (channel_c_id, 2), - // (channel_d_id, 3), - // (channel_d_id, 2), - // (channel_ga_id, 2), - // ], - // ); } #[derive(Debug, PartialEq)] @@ -1667,7 +1481,7 @@ fn assert_channels( ) { let actual = channel_store.read_with(cx, |store, _| { store - .channel_dag_entries() + .ordered_channels() .map(|(depth, channel)| ExpectedChannel { depth, name: channel.name.clone(), @@ -1689,7 +1503,7 @@ fn assert_channels_list_shape( let actual = channel_store.read_with(cx, |store, _| { store - .channel_dag_entries() + .ordered_channels() .map(|(depth, channel)| (channel.id, depth)) .collect::>() }); diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index 64fecd5e62..38bc3f7c12 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -83,7 +83,7 @@ impl RandomizedTest for RandomChannelBufferTest { match rng.gen_range(0..100_u32) { 0..=29 => { let channel_name = client.channel_store().read_with(cx, |store, cx| { - store.channel_dag_entries().find_map(|(_, channel)| { + store.ordered_channels().find_map(|(_, channel)| { if store.has_open_channel_buffer(channel.id, cx) { None } else { @@ -131,7 +131,7 @@ impl RandomizedTest for RandomChannelBufferTest { ChannelBufferOperation::JoinChannelNotes { channel_name } => { let buffer = client.channel_store().update(cx, |store, cx| { let channel_id = store - .channel_dag_entries() + .ordered_channels() .find(|(_, c)| c.name == channel_name) .unwrap() .1 diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 8c33727cfb..51eab1eb3f 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -9,7 +9,7 @@ use crate::{ }; use anyhow::Result; use call::ActiveCall; -use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore}; +use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; use channel_modal::ChannelModal; use client::{ proto::{self, PeerId}, @@ -55,17 +55,17 @@ use workspace::{ #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct ToggleCollapse { - location: ChannelPath, + location: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct NewChannel { - location: ChannelPath, + location: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct RenameChannel { - location: ChannelPath, + channel_id: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -111,13 +111,6 @@ pub struct CopyChannelLink { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct StartMoveChannelFor { channel_id: ChannelId, - parent_id: Option, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct StartLinkChannelFor { - channel_id: ChannelId, - parent_id: Option, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -125,8 +118,6 @@ struct MoveChannel { to: ChannelId, } -type DraggedChannel = (Channel, Option); - actions!( collab_panel, [ @@ -163,14 +154,6 @@ impl_actions!( #[derive(Debug, Copy, Clone, PartialEq, Eq)] struct ChannelMoveClipboard { channel_id: ChannelId, - parent_id: Option, - intent: ClipboardIntent, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum ClipboardIntent { - Move, - // Link, } const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; @@ -217,19 +200,15 @@ pub fn init(cx: &mut AppContext) { _: &mut ViewContext| { panel.channel_clipboard = Some(ChannelMoveClipboard { channel_id: action.channel_id, - parent_id: action.parent_id, - intent: ClipboardIntent::Move, }); }, ); cx.add_action( |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext| { - if let Some((_, path)) = panel.selected_channel() { + if let Some(channel) = panel.selected_channel() { panel.channel_clipboard = Some(ChannelMoveClipboard { - channel_id: path.channel_id(), - parent_id: path.parent_id(), - intent: ClipboardIntent::Move, + channel_id: channel.id, }) } }, @@ -237,29 +216,19 @@ pub fn init(cx: &mut AppContext) { cx.add_action( |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext| { - let clipboard = panel.channel_clipboard.take(); - if let Some(((selected_channel, _), clipboard)) = - panel.selected_channel().zip(clipboard) - { - match clipboard.intent { - ClipboardIntent::Move => panel.channel_store.update(cx, |channel_store, cx| { - match clipboard.parent_id { - Some(parent_id) => channel_store.move_channel( - clipboard.channel_id, - parent_id, - selected_channel.id, - cx, - ), - None => channel_store.link_channel( - clipboard.channel_id, - selected_channel.id, - cx, - ), - } - .detach_and_log_err(cx) - }), - } - } + let Some(clipboard) = panel.channel_clipboard.take() else { + return; + }; + let Some(selected_channel) = panel.selected_channel() else { + return; + }; + + panel + .channel_store + .update(cx, |channel_store, cx| { + channel_store.move_channel(clipboard.channel_id, selected_channel.id, cx) + }) + .detach_and_log_err(cx) }, ); @@ -267,16 +236,9 @@ pub fn init(cx: &mut AppContext) { |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext| { if let Some(clipboard) = panel.channel_clipboard.take() { panel.channel_store.update(cx, |channel_store, cx| { - match clipboard.parent_id { - Some(parent_id) => channel_store.move_channel( - clipboard.channel_id, - parent_id, - action.to, - cx, - ), - None => channel_store.link_channel(clipboard.channel_id, action.to, cx), - } - .detach_and_log_err(cx) + channel_store + .move_channel(clipboard.channel_id, action.to, cx) + .detach_and_log_err(cx) }) } }, @@ -286,11 +248,11 @@ pub fn init(cx: &mut AppContext) { #[derive(Debug)] pub enum ChannelEditingState { Create { - location: Option, + location: Option, pending_name: Option, }, Rename { - location: ChannelPath, + location: ChannelId, pending_name: Option, }, } @@ -324,8 +286,8 @@ pub struct CollabPanel { list_state: ListState, subscriptions: Vec, collapsed_sections: Vec
, - collapsed_channels: Vec, - drag_target_channel: Option, + collapsed_channels: Vec, + drag_target_channel: Option, workspace: WeakViewHandle, context_menu_on_selected: bool, } @@ -333,7 +295,7 @@ pub struct CollabPanel { #[derive(Serialize, Deserialize)] struct SerializedCollabPanel { width: Option, - collapsed_channels: Option>, + collapsed_channels: Option>, } #[derive(Debug)] @@ -378,7 +340,7 @@ enum ListEntry { Channel { channel: Arc, depth: usize, - path: ChannelPath, + has_children: bool, }, ChannelNotes { channel_id: ChannelId, @@ -510,20 +472,9 @@ impl CollabPanel { cx, ) } - ListEntry::Channel { - channel, - depth, - path, - } => { - let channel_row = this.render_channel( - &*channel, - *depth, - path.to_owned(), - &theme, - is_selected, - ix, - cx, - ); + ListEntry::Channel { channel, depth, .. } => { + let channel_row = + this.render_channel(&*channel, *depth, &theme, is_selected, ix, cx); if is_selected && this.context_menu_on_selected { Stack::new() @@ -879,7 +830,7 @@ impl CollabPanel { if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { self.match_candidates.clear(); self.match_candidates - .extend(channel_store.channel_dag_entries().enumerate().map( + .extend(channel_store.ordered_channels().enumerate().map( |(ix, (_, channel))| StringMatchCandidate { id: ix, string: channel.name.clone(), @@ -901,48 +852,52 @@ impl CollabPanel { } let mut collapse_depth = None; for mat in matches { - let (channel, path) = channel_store - .channel_dag_entry_at(mat.candidate_id) - .unwrap(); - let depth = path.len() - 1; + let channel = channel_store.channel_at_index(mat.candidate_id).unwrap(); + let depth = channel.parent_path.len(); - if collapse_depth.is_none() && self.is_channel_collapsed(path) { + if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { collapse_depth = Some(depth); } else if let Some(collapsed_depth) = collapse_depth { if depth > collapsed_depth { continue; } - if self.is_channel_collapsed(path) { + if self.is_channel_collapsed(channel.id) { collapse_depth = Some(depth); } else { collapse_depth = None; } } + let has_children = channel_store + .channel_at_index(mat.candidate_id + 1) + .map_or(false, |next_channel| { + next_channel.parent_path.ends_with(&[channel.id]) + }); + match &self.channel_editing_state { Some(ChannelEditingState::Create { - location: parent_path, + location: parent_id, .. - }) if parent_path.as_ref() == Some(path) => { + }) if *parent_id == Some(channel.id) => { self.entries.push(ListEntry::Channel { channel: channel.clone(), depth, - path: path.clone(), + has_children: false, }); self.entries .push(ListEntry::ChannelEditor { depth: depth + 1 }); } Some(ChannelEditingState::Rename { - location: parent_path, + location: parent_id, .. - }) if parent_path == path => { + }) if parent_id == &channel.id => { self.entries.push(ListEntry::ChannelEditor { depth }); } _ => { self.entries.push(ListEntry::Channel { channel: channel.clone(), depth, - path: path.clone(), + has_children, }); } } @@ -1910,7 +1865,6 @@ impl CollabPanel { &self, channel: &Channel, depth: usize, - path: ChannelPath, theme: &theme::Theme, is_selected: bool, ix: usize, @@ -1918,16 +1872,16 @@ impl CollabPanel { ) -> AnyElement { let channel_id = channel.id; let collab_theme = &theme.collab_panel; - let has_children = self.channel_store.read(cx).has_children(channel_id); + let has_children = self.channel_store.read(cx).channel_has_children(); let is_public = self .channel_store .read(cx) .channel_for_id(channel_id) .map(|channel| channel.visibility) == Some(proto::ChannelVisibility::Public); - let other_selected = - self.selected_channel().map(|channel| channel.0.id) == Some(channel.id); - let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok()); + let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); + let disclosed = + has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); let is_active = iife!({ let call_channel = ActiveCall::global(cx) @@ -1950,12 +1904,14 @@ impl CollabPanel { let mut is_dragged_over = false; if cx .global::>() - .currently_dragged::(cx.window()) + .currently_dragged::(cx.window()) .is_some() && self .drag_target_channel .as_ref() - .filter(|(_, dragged_path)| path.starts_with(dragged_path)) + .filter(|channel_id| { + channel.parent_path.contains(channel_id) || channel.id == **channel_id + }) .is_some() { is_dragged_over = true; @@ -2139,7 +2095,7 @@ impl CollabPanel { .disclosable( disclosed, Box::new(ToggleCollapse { - location: path.clone(), + location: channel.id.clone(), }), ) .with_id(ix) @@ -2168,55 +2124,42 @@ impl CollabPanel { } }) .on_click(MouseButton::Right, { - let path = path.clone(); + let channel = channel.clone(); move |e, this, cx| { - this.deploy_channel_context_menu(Some(e.position), &path, ix, cx); + this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx); } }) .on_up(MouseButton::Left, move |_, this, cx| { if let Some((_, dragged_channel)) = cx .global::>() - .currently_dragged::(cx.window()) + .currently_dragged::>(cx.window()) { - this.channel_store.update(cx, |channel_store, cx| { - match dragged_channel.1 { - Some(parent_id) => channel_store.move_channel( - dragged_channel.0.id, - parent_id, - channel_id, - cx, - ), - None => channel_store.link_channel(dragged_channel.0.id, channel_id, cx), - } + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.move_channel(dragged_channel.id, channel_id, cx) + }) .detach_and_log_err(cx) - }) } }) .on_move({ let channel = channel.clone(); - let path = path.clone(); move |_, this, cx| { - if let Some((_, _dragged_channel)) = - cx.global::>() - .currently_dragged::(cx.window()) + if let Some((_, dragged_channel_id)) = cx + .global::>() + .currently_dragged::(cx.window()) { - match &this.drag_target_channel { - Some(current_target) - if current_target.0 == channel && current_target.1 == path => - { - return - } - _ => { - this.drag_target_channel = Some((channel.clone(), path.clone())); - cx.notify(); - } + if this.drag_target_channel != Some(*dragged_channel_id) { + this.drag_target_channel = Some(channel.id); + } else { + this.drag_target_channel = None; } + cx.notify() } } }) .as_draggable( - (channel.clone(), path.parent_id()), - move |_, (channel, _), cx: &mut ViewContext| { + channel.clone(), + move |_, channel, cx: &mut ViewContext| { let theme = &theme::current(cx).collab_panel; Flex::::row() @@ -2551,39 +2494,29 @@ impl CollabPanel { } fn has_subchannels(&self, ix: usize) -> bool { - self.entries - .get(ix) - .zip(self.entries.get(ix + 1)) - .map(|entries| match entries { - ( - ListEntry::Channel { - path: this_path, .. - }, - ListEntry::Channel { - path: next_path, .. - }, - ) => next_path.starts_with(this_path), - _ => false, - }) - .unwrap_or(false) + self.entries.get(ix).map_or(false, |entry| { + if let ListEntry::Channel { has_children, .. } = entry { + *has_children + } else { + false + } + }) } fn deploy_channel_context_menu( &mut self, position: Option, - path: &ChannelPath, + channel: &Channel, ix: usize, cx: &mut ViewContext, ) { self.context_menu_on_selected = position.is_none(); - let channel_name = self.channel_clipboard.as_ref().and_then(|channel| { - let channel_name = self - .channel_store + let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| { + self.channel_store .read(cx) - .channel_for_id(channel.channel_id) - .map(|channel| channel.name.clone())?; - Some(channel_name) + .channel_for_id(clipboard.channel_id) + .map(|channel| channel.name.clone()) }); self.context_menu.update(cx, |context_menu, cx| { @@ -2607,7 +2540,7 @@ impl CollabPanel { )); if self.has_subchannels(ix) { - let expand_action_name = if self.is_channel_collapsed(&path) { + let expand_action_name = if self.is_channel_collapsed(channel.id) { "Expand Subchannels" } else { "Collapse Subchannels" @@ -2615,7 +2548,7 @@ impl CollabPanel { items.push(ContextMenuItem::action( expand_action_name, ToggleCollapse { - location: path.clone(), + location: channel.id, }, )); } @@ -2623,61 +2556,52 @@ impl CollabPanel { items.push(ContextMenuItem::action( "Open Notes", OpenChannelNotes { - channel_id: path.channel_id(), + channel_id: channel.id, }, )); items.push(ContextMenuItem::action( "Open Chat", JoinChannelChat { - channel_id: path.channel_id(), + channel_id: channel.id, }, )); items.push(ContextMenuItem::action( "Copy Channel Link", CopyChannelLink { - channel_id: path.channel_id(), + channel_id: channel.id, }, )); - if self - .channel_store - .read(cx) - .is_channel_admin(path.channel_id()) - { - let parent_id = path.parent_id(); - + if self.channel_store.read(cx).is_channel_admin(channel.id) { items.extend([ ContextMenuItem::Separator, ContextMenuItem::action( "New Subchannel", NewChannel { - location: path.clone(), + location: channel.id, }, ), ContextMenuItem::action( "Rename", RenameChannel { - location: path.clone(), + channel_id: channel.id, }, ), ContextMenuItem::action( "Move this channel", StartMoveChannelFor { - channel_id: path.channel_id(), - parent_id, + channel_id: channel.id, }, ), ]); - if let Some(channel_name) = channel_name { + if let Some(channel_name) = clipboard_channel_name { items.push(ContextMenuItem::Separator); items.push(ContextMenuItem::action( format!("Move '#{}' here", channel_name), - MoveChannel { - to: path.channel_id(), - }, + MoveChannel { to: channel.id }, )); } @@ -2686,20 +2610,20 @@ impl CollabPanel { ContextMenuItem::action( "Invite Members", InviteMembers { - channel_id: path.channel_id(), + channel_id: channel.id, }, ), ContextMenuItem::action( "Manage Members", ManageMembers { - channel_id: path.channel_id(), + channel_id: channel.id, }, ), ContextMenuItem::Separator, ContextMenuItem::action( "Delete", RemoveChannel { - channel_id: path.channel_id(), + channel_id: channel.id, }, ), ]); @@ -2870,11 +2794,7 @@ impl CollabPanel { self.channel_store .update(cx, |channel_store, cx| { - channel_store.create_channel( - &channel_name, - location.as_ref().map(|location| location.channel_id()), - cx, - ) + channel_store.create_channel(&channel_name, *location, cx) }) .detach(); cx.notify(); @@ -2891,7 +2811,7 @@ impl CollabPanel { self.channel_store .update(cx, |channel_store, cx| { - channel_store.rename(location.channel_id(), &channel_name, cx) + channel_store.rename(*location, &channel_name, cx) }) .detach(); cx.notify(); @@ -2918,33 +2838,27 @@ impl CollabPanel { _: &CollapseSelectedChannel, cx: &mut ViewContext, ) { - let Some((_, path)) = self - .selected_channel() - .map(|(channel, parent)| (channel.id, parent)) - else { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { return; }; - if self.is_channel_collapsed(&path) { + if self.is_channel_collapsed(channel_id) { return; } - self.toggle_channel_collapsed(&path.clone(), cx); + self.toggle_channel_collapsed(channel_id, cx); } fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { - let Some((_, path)) = self - .selected_channel() - .map(|(channel, parent)| (channel.id, parent)) - else { + let Some(id) = self.selected_channel().map(|channel| channel.id) else { return; }; - if !self.is_channel_collapsed(&path) { + if !self.is_channel_collapsed(id) { return; } - self.toggle_channel_collapsed(path.to_owned(), cx) + self.toggle_channel_collapsed(id, cx) } fn toggle_channel_collapsed_action( @@ -2952,21 +2866,16 @@ impl CollabPanel { action: &ToggleCollapse, cx: &mut ViewContext, ) { - self.toggle_channel_collapsed(&action.location, cx); + self.toggle_channel_collapsed(action.location, cx); } - fn toggle_channel_collapsed<'a>( - &mut self, - path: impl Into>, - cx: &mut ViewContext, - ) { - let path = path.into(); - match self.collapsed_channels.binary_search(&path) { + fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + match self.collapsed_channels.binary_search(&channel_id) { Ok(ix) => { self.collapsed_channels.remove(ix); } Err(ix) => { - self.collapsed_channels.insert(ix, path.into_owned()); + self.collapsed_channels.insert(ix, channel_id); } }; self.serialize(cx); @@ -2975,8 +2884,8 @@ impl CollabPanel { cx.focus_self(); } - fn is_channel_collapsed(&self, path: &ChannelPath) -> bool { - self.collapsed_channels.binary_search(path).is_ok() + fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { + self.collapsed_channels.binary_search(&channel_id).is_ok() } fn leave_call(cx: &mut ViewContext) { @@ -3039,16 +2948,16 @@ impl CollabPanel { } fn remove(&mut self, _: &Remove, cx: &mut ViewContext) { - if let Some((channel, _)) = self.selected_channel() { + if let Some(channel) = self.selected_channel() { self.remove_channel(channel.id, cx) } } fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { - if let Some((_, parent)) = self.selected_channel() { + if let Some(channel) = self.selected_channel() { self.rename_channel( &RenameChannel { - location: parent.to_owned(), + channel_id: channel.id, }, cx, ); @@ -3057,15 +2966,12 @@ impl CollabPanel { fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext) { let channel_store = self.channel_store.read(cx); - if !channel_store.is_channel_admin(action.location.channel_id()) { + if !channel_store.is_channel_admin(action.channel_id) { return; } - if let Some(channel) = channel_store - .channel_for_id(action.location.channel_id()) - .cloned() - { + if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() { self.channel_editing_state = Some(ChannelEditingState::Rename { - location: action.location.to_owned(), + location: action.channel_id.to_owned(), pending_name: None, }); self.channel_name_editor.update(cx, |editor, cx| { @@ -3085,22 +2991,18 @@ impl CollabPanel { } fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { - let Some((_, path)) = self.selected_channel() else { + let Some(channel) = self.selected_channel() else { return; }; - self.deploy_channel_context_menu(None, &path.to_owned(), self.selection.unwrap(), cx); + self.deploy_channel_context_menu(None, &channel.clone(), self.selection.unwrap(), cx); } - fn selected_channel(&self) -> Option<(&Arc, &ChannelPath)> { + fn selected_channel(&self) -> Option<&Arc> { self.selection .and_then(|ix| self.entries.get(ix)) .and_then(|entry| match entry { - ListEntry::Channel { - channel, - path: parent, - .. - } => Some((channel, parent)), + ListEntry::Channel { channel, .. } => Some(channel), _ => None, }) } @@ -3517,19 +3419,13 @@ impl PartialEq for ListEntry { } } ListEntry::Channel { - channel: channel_1, - depth: depth_1, - path: parent_1, + channel: channel_1, .. } => { if let ListEntry::Channel { - channel: channel_2, - depth: depth_2, - path: parent_2, + channel: channel_2, .. } = other { - return channel_1.id == channel_2.id - && depth_1 == depth_2 - && parent_1 == parent_2; + return channel_1.id == channel_2.id; } } ListEntry::ChannelNotes { channel_id } => { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 9ecdbde66b..fddbf1e50d 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -171,8 +171,6 @@ message Envelope { AckChannelMessage ack_channel_message = 143; GetChannelMessagesById get_channel_messages_by_id = 144; - LinkChannel link_channel = 145; - UnlinkChannel unlink_channel = 146; MoveChannel move_channel = 147; SetChannelVisibility set_channel_visibility = 148; @@ -972,8 +970,6 @@ message LspDiskBasedDiagnosticsUpdated {} message UpdateChannels { repeated Channel channels = 1; - repeated ChannelEdge insert_edge = 2; - repeated ChannelEdge delete_edge = 3; repeated uint64 delete_channels = 4; repeated Channel channel_invitations = 5; repeated uint64 remove_channel_invitations = 6; @@ -993,11 +989,6 @@ message UnseenChannelBufferChange { repeated VectorClockEntry version = 3; } -message ChannelEdge { - uint64 channel_id = 1; - uint64 parent_id = 2; -} - message ChannelPermission { uint64 channel_id = 1; ChannelRole role = 3; @@ -1137,20 +1128,9 @@ message GetChannelMessagesById { repeated uint64 message_ids = 1; } -message LinkChannel { - uint64 channel_id = 1; - uint64 to = 2; -} - -message UnlinkChannel { - uint64 channel_id = 1; - uint64 from = 2; -} - message MoveChannel { uint64 channel_id = 1; - uint64 from = 2; - uint64 to = 3; + uint64 to = 2; } message JoinChannelBuffer { @@ -1586,6 +1566,7 @@ message Channel { string name = 2; ChannelVisibility visibility = 3; ChannelRole role = 4; + repeated uint64 parent_path = 5; } message Contact { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index c501c85107..77a69122c2 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -210,7 +210,6 @@ messages!( (LeaveChannelChat, Foreground), (LeaveProject, Foreground), (LeaveRoom, Foreground), - (LinkChannel, Foreground), (MarkNotificationRead, Foreground), (MoveChannel, Foreground), (OnTypeFormatting, Background), @@ -263,7 +262,6 @@ messages!( (SynchronizeBuffersResponse, Foreground), (Test, Foreground), (Unfollow, Foreground), - (UnlinkChannel, Foreground), (UnshareProject, Foreground), (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), @@ -327,7 +325,6 @@ request_messages!( (JoinRoom, JoinRoomResponse), (LeaveChannelBuffer, Ack), (LeaveRoom, Ack), - (LinkChannel, Ack), (MarkNotificationRead, Ack), (MoveChannel, Ack), (OnTypeFormatting, OnTypeFormattingResponse), @@ -362,7 +359,6 @@ request_messages!( (ShareProject, ShareProjectResponse), (SynchronizeBuffers, SynchronizeBuffersResponse), (Test, Test), - (UnlinkChannel, Ack), (UpdateBuffer, Ack), (UpdateParticipantLocation, Ack), (UpdateProject, Ack), From dfc34e582a16c58935037de06fd546140f77597c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 24 Oct 2023 18:54:55 +0200 Subject: [PATCH 33/40] Fix extra race --- crates/call/src/call.rs | 18 ++++++++++++++++++ crates/collab/src/tests/integration_tests.rs | 14 ++------------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 0f34741c1c..ca1a60bd63 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -60,6 +60,12 @@ impl OneAtATime { } }) } + + fn running(&self) -> bool { + self.cancel + .as_ref() + .is_some_and(|cancel| !cancel.is_canceled()) + } } /// Singleton global maintaining the user's participation in a room across workspaces. @@ -170,6 +176,10 @@ impl ActiveCall { } cx.notify(); + if self._join_debouncer.running() { + return Task::ready(Ok(())); + } + let room = if let Some(room) = self.room().cloned() { Some(Task::ready(Ok(room)).shared()) } else { @@ -286,6 +296,10 @@ impl ActiveCall { return Task::ready(Err(anyhow!("no incoming call"))); }; + if self.pending_room_creation.is_some() { + return Task::ready(Ok(())); + } + let room_id = call.room_id.clone(); let client = self.client.clone(); let user_store = self.user_store.clone(); @@ -331,6 +345,10 @@ impl ActiveCall { } } + if self.pending_room_creation.is_some() { + return Task::ready(Ok(None)); + } + let client = self.client.clone(); let user_store = self.user_store.clone(); let join = self._join_debouncer.spawn(cx, move |cx| async move { diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index a3546b516d..c0ba9ca25e 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -540,7 +540,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( b_invite.await.unwrap(); c_invite.await.unwrap(); - assert!(join_channel.await.is_err()); + join_channel.await.unwrap(); let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); deterministic.run_until_parked(); @@ -578,18 +578,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( b_invite.await.unwrap(); c_invite.await.unwrap(); - let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); deterministic.run_until_parked(); - - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: Default::default(), - pending: vec!["user_b".to_string(), "user_c".to_string()] - } - ); - - assert_eq!(channel_id(&room_a, cx_a), Some(channel_1)); } #[gpui::test(iterations = 10)] From 1c5b32105877ecc07ab470a6aebbf3398bf50eb3 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 24 Oct 2023 19:29:44 +0200 Subject: [PATCH 34/40] Allow completion menus to be cycled --- crates/editor/src/editor.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ef2c459aad..101d2297a9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -966,8 +966,11 @@ impl CompletionsMenu { ) { if self.selected_item > 0 { self.selected_item -= 1; + } else { + self.selected_item = self.matches.len() - 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -979,8 +982,10 @@ impl CompletionsMenu { ) { if self.selected_item + 1 < self.matches.len() { self.selected_item += 1; - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + } else { + self.selected_item = 0; } + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -1532,17 +1537,23 @@ impl CodeActionsMenu { fn select_prev(&mut self, cx: &mut ViewContext) { if self.selected_item > 0 { self.selected_item -= 1; + } else { + self.selected_item = self.actions.len() - 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - cx.notify() } + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + cx.notify(); } fn select_next(&mut self, cx: &mut ViewContext) { if self.selected_item + 1 < self.actions.len() { self.selected_item += 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - cx.notify() + } else { + self.selected_item = 0; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } + cx.notify(); } fn select_last(&mut self, cx: &mut ViewContext) { From 1411b98a5dc074caef07ee6e6a1badcbca90e760 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 24 Oct 2023 19:48:31 +0200 Subject: [PATCH 35/40] link to channel notes --- crates/workspace/src/workspace.rs | 62 +++++++++++++++++++++---------- crates/zed/src/main.rs | 31 +++++++++++++++- crates/zed/src/open_listener.rs | 9 ++++- 3 files changed, 80 insertions(+), 22 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e879b981ef..b212058d58 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -35,9 +35,9 @@ use gpui::{ CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds, WindowOptions, }, - AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AnyWindowHandle, AppContext, AsyncAppContext, - Entity, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, - ViewHandle, WeakViewHandle, WindowContext, WindowHandle, + AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, + ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, + WeakViewHandle, WindowContext, WindowHandle, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; use itertools::Itertools; @@ -4295,12 +4295,14 @@ pub fn join_channel( } if let Err(err) = result { - let prompt = active_window.unwrap().prompt( - PromptLevel::Critical, - &format!("Failed to join channel: {}", err), - &["Ok"], - &mut cx, - ); + let prompt = active_window.unwrap().update(&mut cx, |_, cx| { + cx.prompt( + PromptLevel::Critical, + &format!("Failed to join channel: {}", err), + &["Ok"], + ) + }); + if let Some(mut prompt) = prompt { prompt.next().await; } else { @@ -4313,17 +4315,39 @@ pub fn join_channel( }) } -pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option { +pub async fn get_any_active_workspace( + app_state: Arc, + mut cx: AsyncAppContext, +) -> Result> { + // find an existing workspace to focus and show call controls + let active_window = activate_any_workspace_window(&mut cx); + if active_window.is_none() { + cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx)) + .await; + } + + let Some(active_window) = activate_any_workspace_window(&mut cx) else { + return Err(anyhow!("could not open zed"))?; + }; + + Ok(active_window) +} + +pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option> { for window in cx.windows() { - let found = window.update(cx, |cx| { - let is_workspace = cx.root_view().clone().downcast::().is_some(); - if is_workspace { - cx.activate_window(); - } - is_workspace - }); - if found == Some(true) { - return Some(window); + if let Some(workspace) = window + .update(cx, |cx| { + cx.root_view() + .clone() + .downcast::() + .map(|workspace| { + cx.activate_window(); + workspace + }) + }) + .flatten() + { + return Some(workspace); } } None diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index a590db098a..5add524414 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -7,6 +7,7 @@ use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::{ self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN, }; +use collab_ui::channel_view::ChannelView; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use futures::StreamExt; @@ -240,6 +241,20 @@ fn main() { }) .detach_and_log_err(cx) } + Ok(Some(OpenRequest::OpenChannelNotes { channel_id })) => { + triggered_authentication = true; + let app_state = app_state.clone(); + let client = client.clone(); + cx.spawn(|mut cx| async move { + // ignore errors here, we'll show a generic "not signed in" + let _ = authenticate(client, &cx).await; + let workspace = + workspace::get_any_active_workspace(app_state, cx.clone()).await?; + cx.update(|cx| ChannelView::open(channel_id, workspace, cx)) + .await + }) + .detach_and_log_err(cx) + } Ok(None) | Err(_) => cx .spawn({ let app_state = app_state.clone(); @@ -254,8 +269,10 @@ fn main() { while let Some(request) = open_rx.next().await { match request { OpenRequest::Paths { paths } => { - cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - .detach(); + cx.update(|cx| { + workspace::open_paths(&paths, &app_state.clone(), None, cx) + }) + .detach(); } OpenRequest::CliConnection { connection } => { cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) @@ -266,6 +283,16 @@ fn main() { workspace::join_channel(channel_id, app_state.clone(), None, cx) }) .detach(), + OpenRequest::OpenChannelNotes { channel_id } => { + let app_state = app_state.clone(); + if let Ok(workspace) = + workspace::get_any_active_workspace(app_state, cx.clone()).await + { + cx.update(|cx| { + ChannelView::open(channel_id, workspace, cx).detach(); + }) + } + } } } } diff --git a/crates/zed/src/open_listener.rs b/crates/zed/src/open_listener.rs index 578d8cd69f..e0b360d0d7 100644 --- a/crates/zed/src/open_listener.rs +++ b/crates/zed/src/open_listener.rs @@ -32,6 +32,9 @@ pub enum OpenRequest { JoinChannel { channel_id: u64, }, + OpenChannelNotes { + channel_id: u64, + }, } pub struct OpenListener { @@ -85,7 +88,11 @@ impl OpenListener { if let Some(slug) = parts.next() { if let Some(id_str) = slug.split("-").last() { if let Ok(channel_id) = id_str.parse::() { - return Some(OpenRequest::JoinChannel { channel_id }); + if Some("notes") == parts.next() { + return Some(OpenRequest::OpenChannelNotes { channel_id }); + } else { + return Some(OpenRequest::JoinChannel { channel_id }); + } } } } From 6f173c64b3f1596fdf7c2f89ab7fffd149767dc0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 25 Oct 2023 09:22:06 +0200 Subject: [PATCH 36/40] Fix tests by re-instating paths in the new format --- crates/channel/src/channel_store_tests.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 43e0344b2c..ff8761ee91 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -53,14 +53,14 @@ fn test_update_channels(cx: &mut AppContext) { name: "x".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Admin.into(), - parent_path: Vec::new(), + parent_path: vec![1], }, proto::Channel { id: 4, name: "y".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Member.into(), - parent_path: Vec::new(), + parent_path: vec![2], }, ], ..Default::default() @@ -92,21 +92,21 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { name: "a".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Admin.into(), - parent_path: Vec::new(), + parent_path: vec![], }, proto::Channel { id: 1, name: "b".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Admin.into(), - parent_path: Vec::new(), + parent_path: vec![0], }, proto::Channel { id: 2, name: "c".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Admin.into(), - parent_path: Vec::new(), + parent_path: vec![0, 1], }, ], ..Default::default() From 70eeefa1f899f6a0b866c6d2d2318f90cf60f79d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 25 Oct 2023 09:27:17 +0200 Subject: [PATCH 37/40] Fix channel collapsing --- crates/channel/src/channel_store.rs | 7 ------- crates/collab_ui/src/collab_panel.rs | 19 +++++++++++++++---- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 14738e170b..9757bb8092 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -188,13 +188,6 @@ impl ChannelStore { self.client.clone() } - pub fn channel_has_children(&self) -> bool { - self.channel_index - .by_id() - .iter() - .any(|(_, channel)| channel.parent_path.contains(&channel.id)) - } - /// Returns the number of unique channels in the store pub fn channel_count(&self) -> usize { self.channel_index.by_id().len() diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 51eab1eb3f..66962b0402 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -472,9 +472,20 @@ impl CollabPanel { cx, ) } - ListEntry::Channel { channel, depth, .. } => { - let channel_row = - this.render_channel(&*channel, *depth, &theme, is_selected, ix, cx); + ListEntry::Channel { + channel, + depth, + has_children, + } => { + let channel_row = this.render_channel( + &*channel, + *depth, + &theme, + is_selected, + *has_children, + ix, + cx, + ); if is_selected && this.context_menu_on_selected { Stack::new() @@ -1867,12 +1878,12 @@ impl CollabPanel { depth: usize, theme: &theme::Theme, is_selected: bool, + has_children: bool, ix: usize, cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; let collab_theme = &theme.collab_panel; - let has_children = self.channel_store.read(cx).channel_has_children(); let is_public = self .channel_store .read(cx) From 42259a400742b510435649b26dd6a1d2333cc24a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 24 Oct 2023 17:48:18 +0200 Subject: [PATCH 38/40] Fix channel dragging Co-authored-by: Conrad Co-authored-by: Joseph --- crates/collab_ui/src/collab_panel.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 66962b0402..16f8fb5d02 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2143,7 +2143,7 @@ impl CollabPanel { .on_up(MouseButton::Left, move |_, this, cx| { if let Some((_, dragged_channel)) = cx .global::>() - .currently_dragged::>(cx.window()) + .currently_dragged::(cx.window()) { this.channel_store .update(cx, |channel_store, cx| { @@ -2155,20 +2155,18 @@ impl CollabPanel { .on_move({ let channel = channel.clone(); move |_, this, cx| { - if let Some((_, dragged_channel_id)) = cx + if let Some((_, dragged_channel)) = cx .global::>() - .currently_dragged::(cx.window()) + .currently_dragged::(cx.window()) { - if this.drag_target_channel != Some(*dragged_channel_id) { + if channel.id != dragged_channel.id { this.drag_target_channel = Some(channel.id); - } else { - this.drag_target_channel = None; } cx.notify() } } }) - .as_draggable( + .as_draggable::<_, Channel>( channel.clone(), move |_, channel, cx: &mut ViewContext| { let theme = &theme::current(cx).collab_panel; From 32367eba14f3b08464e2e39c7331d353b2e69a4b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2023 15:39:02 +0200 Subject: [PATCH 39/40] Set up UI to allow dragging a channel to the root --- crates/channel/src/channel_store.rs | 2 +- crates/collab/src/db/queries/channels.rs | 24 ++--- crates/collab/src/db/tests/channel_tests.rs | 2 +- crates/collab/src/rpc.rs | 2 +- crates/collab/src/tests/channel_tests.rs | 6 +- crates/collab_ui/src/collab_panel.rs | 111 ++++++++++++++------ crates/rpc/proto/zed.proto | 2 +- crates/theme/src/theme.rs | 1 + styles/src/style_tree/collab_panel.ts | 10 +- styles/src/style_tree/search.ts | 5 +- 10 files changed, 107 insertions(+), 58 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 9757bb8092..efa05d51a9 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -501,7 +501,7 @@ impl ChannelStore { pub fn move_channel( &mut self, channel_id: ChannelId, - to: ChannelId, + to: Option, cx: &mut ModelContext, ) -> Task> { let client = self.client.clone(); diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index b65e677764..a5c6e4dcfc 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1205,37 +1205,37 @@ impl Database { pub async fn move_channel( &self, channel_id: ChannelId, - new_parent_id: ChannelId, + new_parent_id: Option, admin_id: UserId, ) -> Result> { - // check you're an admin of source and target (and maybe current channel) - // change parent_path on current channel - // change parent_path on all children - self.transaction(|tx| async move { + let Some(new_parent_id) = new_parent_id else { + return Err(anyhow!("not supported"))?; + }; + let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?; + self.check_user_is_channel_admin(&new_parent, admin_id, &*tx) + .await?; let channel = self.get_channel_internal(channel_id, &*tx).await?; self.check_user_is_channel_admin(&channel, admin_id, &*tx) .await?; - self.check_user_is_channel_admin(&new_parent, admin_id, &*tx) - .await?; let previous_participants = self .get_channel_participant_details_internal(&channel, &*tx) .await?; let old_path = format!("{}{}/", channel.parent_path, channel.id); - let new_parent_path = format!("{}{}/", new_parent.parent_path, new_parent_id); + let new_parent_path = format!("{}{}/", new_parent.parent_path, new_parent.id); let new_path = format!("{}{}/", new_parent_path, channel.id); if old_path == new_path { return Ok(None); } - let mut channel = channel.into_active_model(); - channel.parent_path = ActiveValue::Set(new_parent_path); - channel.save(&*tx).await?; + let mut model = channel.into_active_model(); + model.parent_path = ActiveValue::Set(new_parent_path); + model.update(&*tx).await?; let descendent_ids = ChannelId::find_by_statement::(Statement::from_sql_and_values( @@ -1250,7 +1250,7 @@ impl Database { .all(&*tx) .await?; - let participants_to_update: HashMap = self + let participants_to_update: HashMap<_, _> = self .participants_to_notify_for_channel_change(&new_parent, &*tx) .await? .into_iter() diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 936765b8c9..0d486003bc 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -424,7 +424,7 @@ async fn test_db_channel_moving_bugs(db: &Arc) { // Move to same parent should be a no-op assert!(db - .move_channel(projects_id, zed_id, user_id) + .move_channel(projects_id, Some(zed_id), user_id) .await .unwrap() .is_none()); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 01e8530e67..a0ec7da392 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2476,7 +2476,7 @@ async fn move_channel( session: Session, ) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); - let to = ChannelId::from_proto(request.to); + let to = request.to.map(ChannelId::from_proto); let result = session .db() diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index d2c5e1cec3..a33ded6492 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1016,7 +1016,7 @@ async fn test_channel_link_notifications( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.move_channel(vim_channel, active_channel, cx) + channel_store.move_channel(vim_channel, Some(active_channel), cx) }) .await .unwrap(); @@ -1051,7 +1051,7 @@ async fn test_channel_link_notifications( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.move_channel(helix_channel, vim_channel, cx) + channel_store.move_channel(helix_channel, Some(vim_channel), cx) }) .await .unwrap(); @@ -1424,7 +1424,7 @@ async fn test_channel_moving( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.move_channel(channel_d_id, channel_b_id, cx) + channel_store.move_channel(channel_d_id, Some(channel_b_id), cx) }) .await .unwrap(); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 16f8fb5d02..8d68ee12c0 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -226,7 +226,7 @@ pub fn init(cx: &mut AppContext) { panel .channel_store .update(cx, |channel_store, cx| { - channel_store.move_channel(clipboard.channel_id, selected_channel.id, cx) + channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx) }) .detach_and_log_err(cx) }, @@ -237,7 +237,7 @@ pub fn init(cx: &mut AppContext) { if let Some(clipboard) = panel.channel_clipboard.take() { panel.channel_store.update(cx, |channel_store, cx| { channel_store - .move_channel(clipboard.channel_id, action.to, cx) + .move_channel(clipboard.channel_id, Some(action.to), cx) .detach_and_log_err(cx) }) } @@ -287,11 +287,18 @@ pub struct CollabPanel { subscriptions: Vec, collapsed_sections: Vec
, collapsed_channels: Vec, - drag_target_channel: Option, + drag_target_channel: ChannelDragTarget, workspace: WeakViewHandle, context_menu_on_selected: bool, } +#[derive(PartialEq, Eq)] +enum ChannelDragTarget { + None, + Root, + Channel(ChannelId), +} + #[derive(Serialize, Deserialize)] struct SerializedCollabPanel { width: Option, @@ -577,7 +584,7 @@ impl CollabPanel { workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), context_menu_on_selected: true, - drag_target_channel: None, + drag_target_channel: ChannelDragTarget::None, list_state, }; @@ -1450,6 +1457,7 @@ impl CollabPanel { let mut channel_link = None; let mut channel_tooltip_text = None; let mut channel_icon = None; + let mut is_dragged_over = false; let text = match section { Section::ActiveCall => { @@ -1533,26 +1541,37 @@ impl CollabPanel { cx, ), ), - Section::Channels => Some( - MouseEventHandler::new::(0, cx, |state, _| { - render_icon_button( - theme - .collab_panel - .add_contact_button - .style_for(is_selected, state), - "icons/plus.svg", - ) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) - .with_tooltip::( - 0, - "Create a channel", - None, - tooltip_style.clone(), - cx, - ), - ), + Section::Channels => { + if cx + .global::>() + .currently_dragged::(cx.window()) + .is_some() + && self.drag_target_channel == ChannelDragTarget::Root + { + is_dragged_over = true; + } + + Some( + MouseEventHandler::new::(0, cx, |state, _| { + render_icon_button( + theme + .collab_panel + .add_contact_button + .style_for(is_selected, state), + "icons/plus.svg", + ) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) + .with_tooltip::( + 0, + "Create a channel", + None, + tooltip_style.clone(), + cx, + ), + ) + } _ => None, }; @@ -1623,9 +1642,37 @@ impl CollabPanel { .constrained() .with_height(theme.collab_panel.row_height) .contained() - .with_style(header_style.container) + .with_style(if is_dragged_over { + theme.collab_panel.dragged_over_header + } else { + header_style.container + }) }); + result = result + .on_move(move |_, this, cx| { + if cx + .global::>() + .currently_dragged::(cx.window()) + .is_some() + { + this.drag_target_channel = ChannelDragTarget::Root; + cx.notify() + } + }) + .on_up(MouseButton::Left, move |_, this, cx| { + if let Some((_, dragged_channel)) = cx + .global::>() + .currently_dragged::(cx.window()) + { + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.move_channel(dragged_channel.id, None, cx) + }) + .detach_and_log_err(cx) + } + }); + if can_collapse { result = result .with_cursor_style(CursorStyle::PointingHand) @@ -1917,13 +1964,7 @@ impl CollabPanel { .global::>() .currently_dragged::(cx.window()) .is_some() - && self - .drag_target_channel - .as_ref() - .filter(|channel_id| { - channel.parent_path.contains(channel_id) || channel.id == **channel_id - }) - .is_some() + && self.drag_target_channel == ChannelDragTarget::Channel(channel_id) { is_dragged_over = true; } @@ -2126,7 +2167,7 @@ impl CollabPanel { ) }) .on_click(MouseButton::Left, move |_, this, cx| { - if this.drag_target_channel.take().is_none() { + if this.drag_target_channel == ChannelDragTarget::None { if is_active { this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) } else { @@ -2147,7 +2188,7 @@ impl CollabPanel { { this.channel_store .update(cx, |channel_store, cx| { - channel_store.move_channel(dragged_channel.id, channel_id, cx) + channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) }) .detach_and_log_err(cx) } @@ -2160,7 +2201,7 @@ impl CollabPanel { .currently_dragged::(cx.window()) { if channel.id != dragged_channel.id { - this.drag_target_channel = Some(channel.id); + this.drag_target_channel = ChannelDragTarget::Channel(channel.id); } cx.notify() } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index fddbf1e50d..206777879b 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1130,7 +1130,7 @@ message GetChannelMessagesById { message MoveChannel { uint64 channel_id = 1; - uint64 to = 2; + optional uint64 to = 2; } message JoinChannelBuffer { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 3f4264886f..e4b8c02eca 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -250,6 +250,7 @@ pub struct CollabPanel { pub add_contact_button: Toggleable>, pub add_channel_button: Toggleable>, pub header_row: ContainedText, + pub dragged_over_header: ContainerStyle, pub subheader_row: Toggleable>, pub leave_call: Interactive, pub contact_row: Toggleable>, diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 2a7702842a..272b6055ed 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -210,6 +210,14 @@ export default function contacts_panel(): any { right: SPACING, }, }, + dragged_over_header: { + margin: { top: SPACING }, + padding: { + left: SPACING, + right: SPACING, + }, + background: background(layer, "hovered"), + }, subheader_row, leave_call: interactive({ base: { @@ -279,7 +287,7 @@ export default function contacts_panel(): any { margin: { left: CHANNEL_SPACING, }, - } + }, }, list_empty_label_container: { margin: { diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index b0ac023c09..2317108bde 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -2,7 +2,6 @@ import { with_opacity } from "../theme/color" import { background, border, foreground, text } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" -import { text_button } from "../component/text_button" const search_results = () => { const theme = useTheme() @@ -36,7 +35,7 @@ export default function search(): any { left: 10, right: 4, }, - margin: { right: SEARCH_ROW_SPACING } + margin: { right: SEARCH_ROW_SPACING }, } const include_exclude_editor = { @@ -378,7 +377,7 @@ export default function search(): any { modes_container: { padding: { right: SEARCH_ROW_SPACING, - } + }, }, replace_icon: { icon: { From b5cbfb8f1ddddd40a6e9a78194dd51a97d5eda50 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2023 15:50:37 +0200 Subject: [PATCH 40/40] Allow moving channels to the root --- crates/collab/src/db/queries/channels.rs | 42 +++++++++++++++------ crates/collab/src/db/tests/channel_tests.rs | 12 ++++++ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index a5c6e4dcfc..68b06e435d 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1209,24 +1209,29 @@ impl Database { admin_id: UserId, ) -> Result> { self.transaction(|tx| async move { - let Some(new_parent_id) = new_parent_id else { - return Err(anyhow!("not supported"))?; - }; - - let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?; - self.check_user_is_channel_admin(&new_parent, admin_id, &*tx) - .await?; let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_admin(&channel, admin_id, &*tx) .await?; + let new_parent_path; + let new_parent_channel; + if let Some(new_parent_id) = new_parent_id { + let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?; + self.check_user_is_channel_admin(&new_parent, admin_id, &*tx) + .await?; + + new_parent_path = new_parent.path(); + new_parent_channel = Some(new_parent); + } else { + new_parent_path = String::new(); + new_parent_channel = None; + }; + let previous_participants = self .get_channel_participant_details_internal(&channel, &*tx) .await?; let old_path = format!("{}{}/", channel.parent_path, channel.id); - let new_parent_path = format!("{}{}/", new_parent.parent_path, new_parent.id); let new_path = format!("{}{}/", new_parent_path, channel.id); if old_path == new_path { @@ -1235,7 +1240,19 @@ impl Database { let mut model = channel.into_active_model(); model.parent_path = ActiveValue::Set(new_parent_path); - model.update(&*tx).await?; + let channel = model.update(&*tx).await?; + + if new_parent_channel.is_none() { + channel_member::ActiveModel { + id: ActiveValue::NotSet, + channel_id: ActiveValue::Set(channel_id), + user_id: ActiveValue::Set(admin_id), + accepted: ActiveValue::Set(true), + role: ActiveValue::Set(ChannelRole::Admin), + } + .insert(&*tx) + .await?; + } let descendent_ids = ChannelId::find_by_statement::(Statement::from_sql_and_values( @@ -1251,7 +1268,10 @@ impl Database { .await?; let participants_to_update: HashMap<_, _> = self - .participants_to_notify_for_channel_change(&new_parent, &*tx) + .participants_to_notify_for_channel_change( + new_parent_channel.as_ref().unwrap_or(&channel), + &*tx, + ) .await? .into_iter() .collect(); diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 0d486003bc..43526c7f24 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -438,6 +438,18 @@ async fn test_db_channel_moving_bugs(db: &Arc) { (livestreaming_id, &[zed_id, projects_id]), ], ); + + // Move the project channel to the root + db.move_channel(projects_id, None, user_id).await.unwrap(); + let result = db.get_channels_for_user(user_id).await.unwrap(); + assert_channel_tree( + result.channels, + &[ + (zed_id, &[]), + (projects_id, &[]), + (livestreaming_id, &[projects_id]), + ], + ); } test_both_dbs!(