diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 0c91a5a0794020ba548014a242abce4352572175..d20748609af38dca9410eb83e834c423d16cffb5 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -36,8 +36,12 @@ pub fn routes(rpc_server: &Arc, state: Arc) -> Router, +async fn get_top_users_activity_summary( + Query(params): Query, + Extension(app): Extension>, +) -> Result { + let summary = app + .db + .get_top_users_activity_summary(params.start..params.end, 100) + .await?; + Ok(ErasedJson::pretty(summary)) +} + +async fn get_user_activity_timeline( + Path(user_id): Path, + Query(params): Query, Extension(app): Extension>, ) -> Result { let summary = app .db - .summarize_project_activity(params.start..params.end, 100) + .get_user_activity_timeline(params.start..params.end, UserId(user_id)) .await?; Ok(ErasedJson::pretty(summary)) } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 96c93eb93494b6a7d4030ff105d13fa439ea2178..b14f9b14e4c43f9f880f7f754659316160c40acc 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1,4 +1,4 @@ -use std::{ops::Range, time::Duration}; +use std::{cmp, ops::Range, time::Duration}; use crate::{Error, Result}; use anyhow::{anyhow, Context}; @@ -7,10 +7,10 @@ use axum::http::StatusCode; use collections::HashMap; use futures::StreamExt; use nanoid::nanoid; -use serde::Serialize; +use serde::{Deserialize, Serialize}; pub use sqlx::postgres::PgPoolOptions as DbOptions; use sqlx::{types::Uuid, FromRow, QueryBuilder, Row}; -use time::OffsetDateTime; +use time::{OffsetDateTime, PrimitiveDateTime}; #[async_trait] pub trait Db: Send + Sync { @@ -52,7 +52,7 @@ pub trait Db: Send + Sync { &self, project_id: ProjectId, worktree_id: u64, - extensions: HashMap, + extensions: HashMap, ) -> Result<()>; /// Get the file counts on the given project keyed by their worktree and extension. @@ -63,7 +63,7 @@ pub trait Db: Send + Sync { /// Record which users have been active in which projects during /// a given period of time. - async fn record_project_activity( + async fn record_user_activity( &self, time_period: Range, active_projects: &[(UserId, ProjectId)], @@ -71,12 +71,19 @@ pub trait Db: Send + Sync { /// Get the users that have been most active during the given time period, /// along with the amount of time they have been active in each project. - async fn summarize_project_activity( + async fn get_top_users_activity_summary( &self, time_period: Range, max_user_count: usize, ) -> Result>; + /// Get the project activity for the given user and time period. + async fn get_user_activity_timeline( + &self, + time_period: Range, + user_id: UserId, + ) -> Result>; + async fn get_contacts(&self, id: UserId) -> Result>; async fn has_contact(&self, user_id_a: UserId, user_id_b: UserId) -> Result; async fn send_contact_request(&self, requester_id: UserId, responder_id: UserId) -> Result<()>; @@ -499,7 +506,7 @@ impl Db for PostgresDb { &self, project_id: ProjectId, worktree_id: u64, - extensions: HashMap, + extensions: HashMap, ) -> Result<()> { if extensions.is_empty() { return Ok(()); @@ -557,7 +564,7 @@ impl Db for PostgresDb { Ok(extension_counts) } - async fn record_project_activity( + async fn record_user_activity( &self, time_period: Range, projects: &[(UserId, ProjectId)], @@ -586,7 +593,7 @@ impl Db for PostgresDb { Ok(()) } - async fn summarize_project_activity( + async fn get_top_users_activity_summary( &self, time_period: Range, max_user_count: usize, @@ -596,7 +603,7 @@ impl Db for PostgresDb { project_durations AS ( SELECT user_id, project_id, SUM(duration_millis) AS project_duration FROM project_activity_periods - WHERE $1 <= ended_at AND ended_at <= $2 + WHERE $1 < ended_at AND ended_at <= $2 GROUP BY user_id, project_id ), user_durations AS ( @@ -611,7 +618,7 @@ impl Db for PostgresDb { WHERE user_durations.user_id = project_durations.user_id AND user_durations.user_id = users.id - ORDER BY user_id ASC, project_duration DESC + ORDER BY total_duration DESC, user_id ASC "; let mut rows = sqlx::query_as::<_, (UserId, String, ProjectId, i64)>(query) @@ -641,6 +648,91 @@ impl Db for PostgresDb { Ok(result) } + async fn get_user_activity_timeline( + &self, + time_period: Range, + user_id: UserId, + ) -> Result> { + const COALESCE_THRESHOLD: Duration = Duration::from_secs(30); + + let query = " + SELECT + project_activity_periods.ended_at, + project_activity_periods.duration_millis, + project_activity_periods.project_id, + worktree_extensions.extension, + worktree_extensions.count + FROM project_activity_periods + LEFT OUTER JOIN + worktree_extensions + ON + project_activity_periods.project_id = worktree_extensions.project_id + WHERE + project_activity_periods.user_id = $1 AND + $2 < project_activity_periods.ended_at AND + project_activity_periods.ended_at <= $3 + ORDER BY project_activity_periods.id ASC + "; + + let mut rows = sqlx::query_as::< + _, + ( + PrimitiveDateTime, + i32, + ProjectId, + Option, + Option, + ), + >(query) + .bind(user_id) + .bind(time_period.start) + .bind(time_period.end) + .fetch(&self.pool); + + let mut time_periods: HashMap> = Default::default(); + while let Some(row) = rows.next().await { + let (ended_at, duration_millis, project_id, extension, extension_count) = row?; + let ended_at = ended_at.assume_utc(); + let duration = Duration::from_millis(duration_millis as u64); + let started_at = ended_at - duration; + let project_time_periods = time_periods.entry(project_id).or_default(); + + if let Some(prev_duration) = project_time_periods.last_mut() { + if started_at <= prev_duration.end + COALESCE_THRESHOLD + && ended_at >= prev_duration.start + { + prev_duration.end = cmp::max(prev_duration.end, ended_at); + } else { + project_time_periods.push(UserActivityPeriod { + project_id, + start: started_at, + end: ended_at, + extensions: Default::default(), + }); + } + } else { + project_time_periods.push(UserActivityPeriod { + project_id, + start: started_at, + end: ended_at, + extensions: Default::default(), + }); + } + + if let Some((extension, extension_count)) = extension.zip(extension_count) { + project_time_periods + .last_mut() + .unwrap() + .extensions + .insert(extension, extension_count as usize); + } + } + + let mut durations = time_periods.into_values().flatten().collect::>(); + durations.sort_unstable_by_key(|duration| duration.start); + Ok(durations) + } + // contacts async fn get_contacts(&self, user_id: UserId) -> Result> { @@ -1116,7 +1208,18 @@ impl Db for PostgresDb { macro_rules! id_type { ($name:ident) => { #[derive( - Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type, Serialize, + Clone, + Copy, + Debug, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + sqlx::Type, + Serialize, + Deserialize, )] #[sqlx(transparent)] #[serde(transparent)] @@ -1172,6 +1275,16 @@ pub struct UserActivitySummary { pub project_activity: Vec<(ProjectId, Duration)>, } +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct UserActivityPeriod { + project_id: ProjectId, + #[serde(with = "time::serde::iso8601")] + start: OffsetDateTime, + #[serde(with = "time::serde::iso8601")] + end: OffsetDateTime, + extensions: HashMap, +} + id_type!(OrgId); #[derive(FromRow)] pub struct Org { @@ -1439,29 +1552,36 @@ pub mod tests { let user_2 = db.create_user("user_2", None, false).await.unwrap(); let user_3 = db.create_user("user_3", None, false).await.unwrap(); let project_1 = db.register_project(user_1).await.unwrap(); + db.update_worktree_extensions( + project_1, + 1, + HashMap::from_iter([("rs".into(), 5), ("md".into(), 7)]), + ) + .await + .unwrap(); let project_2 = db.register_project(user_2).await.unwrap(); let t0 = OffsetDateTime::now_utc() - Duration::from_secs(60 * 60); // User 2 opens a project let t1 = t0 + Duration::from_secs(10); - db.record_project_activity(t0..t1, &[(user_2, project_2)]) + db.record_user_activity(t0..t1, &[(user_2, project_2)]) .await .unwrap(); let t2 = t1 + Duration::from_secs(10); - db.record_project_activity(t1..t2, &[(user_2, project_2)]) + db.record_user_activity(t1..t2, &[(user_2, project_2)]) .await .unwrap(); // User 1 joins the project let t3 = t2 + Duration::from_secs(10); - db.record_project_activity(t2..t3, &[(user_2, project_2), (user_1, project_2)]) + db.record_user_activity(t2..t3, &[(user_2, project_2), (user_1, project_2)]) .await .unwrap(); // User 1 opens another project let t4 = t3 + Duration::from_secs(10); - db.record_project_activity( + db.record_user_activity( t3..t4, &[ (user_2, project_2), @@ -1474,7 +1594,7 @@ pub mod tests { // User 3 joins that project let t5 = t4 + Duration::from_secs(10); - db.record_project_activity( + db.record_user_activity( t4..t5, &[ (user_2, project_2), @@ -1488,20 +1608,25 @@ pub mod tests { // User 2 leaves let t6 = t5 + Duration::from_secs(5); - db.record_project_activity(t5..t6, &[(user_1, project_1), (user_3, project_1)]) + db.record_user_activity(t5..t6, &[(user_1, project_1), (user_3, project_1)]) + .await + .unwrap(); + + let t7 = t6 + Duration::from_secs(60); + let t8 = t7 + Duration::from_secs(10); + db.record_user_activity(t7..t8, &[(user_1, project_1)]) .await .unwrap(); - let summary = db.summarize_project_activity(t0..t6, 10).await.unwrap(); assert_eq!( - summary, + db.get_top_users_activity_summary(t0..t6, 10).await.unwrap(), &[ UserActivitySummary { id: user_1, github_login: "user_1".to_string(), project_activity: vec![ + (project_1, Duration::from_secs(25)), (project_2, Duration::from_secs(30)), - (project_1, Duration::from_secs(25)) ] }, UserActivitySummary { @@ -1516,6 +1641,46 @@ pub mod tests { }, ] ); + assert_eq!( + db.get_user_activity_timeline(t3..t6, user_1).await.unwrap(), + &[ + UserActivityPeriod { + project_id: project_1, + start: t3, + end: t6, + extensions: HashMap::from_iter([("rs".to_string(), 5), ("md".to_string(), 7)]), + }, + UserActivityPeriod { + project_id: project_2, + start: t3, + end: t5, + extensions: Default::default(), + }, + ] + ); + assert_eq!( + db.get_user_activity_timeline(t0..t8, user_1).await.unwrap(), + &[ + UserActivityPeriod { + project_id: project_2, + start: t2, + end: t5, + extensions: Default::default(), + }, + UserActivityPeriod { + project_id: project_1, + start: t3, + end: t6, + extensions: HashMap::from_iter([("rs".to_string(), 5), ("md".to_string(), 7)]), + }, + UserActivityPeriod { + project_id: project_1, + start: t7, + end: t8, + extensions: HashMap::from_iter([("rs".to_string(), 5), ("md".to_string(), 7)]), + }, + ] + ); } #[tokio::test(flavor = "multi_thread")] @@ -2090,7 +2255,7 @@ pub mod tests { background: Arc, pub users: Mutex>, pub projects: Mutex>, - pub worktree_extensions: Mutex>, + pub worktree_extensions: Mutex>, pub orgs: Mutex>, pub org_memberships: Mutex>, pub channels: Mutex>, @@ -2281,7 +2446,7 @@ pub mod tests { &self, project_id: ProjectId, worktree_id: u64, - extensions: HashMap, + extensions: HashMap, ) -> Result<()> { self.background.simulate_random_delay().await; if !self.projects.lock().contains_key(&project_id) { @@ -2304,22 +2469,30 @@ pub mod tests { unimplemented!() } - async fn record_project_activity( + async fn record_user_activity( &self, - _period: Range, + _time_period: Range, _active_projects: &[(UserId, ProjectId)], ) -> Result<()> { unimplemented!() } - async fn summarize_project_activity( + async fn get_top_users_activity_summary( &self, - _period: Range, + _time_period: Range, _limit: usize, ) -> Result> { unimplemented!() } + async fn get_user_activity_timeline( + &self, + _time_period: Range, + _user_id: UserId, + ) -> Result> { + unimplemented!() + } + // contacts async fn get_contacts(&self, id: UserId) -> Result> { diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 4d23c00d42252f0103b3731288a4ec3f959e8d49..ed429ea87c40b89b12ef5439ecdef16063dca898 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -483,14 +483,20 @@ async fn test_offline_projects( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, ) { cx_a.foreground().forbid_parking(); let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).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_a = UserId::from_proto(client_a.user_id().unwrap()); server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .make_contacts(vec![ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + ]) .await; // Set up observers of the project and user stores. Any time either of @@ -584,7 +590,8 @@ async fn test_offline_projects( .await .unwrap(); - // When a project is offline, no information about it is sent to the server. + // When a project is offline, we still create it on the server but is invisible + // to other users. deterministic.run_until_parked(); assert!(server .store @@ -592,7 +599,10 @@ async fn test_offline_projects( .await .project_metadata_for_user(user_a) .is_empty()); - assert!(project.read_with(cx_a, |project, _| project.remote_id().is_none())); + project.read_with(cx_a, |project, _| { + assert!(project.remote_id().is_some()); + assert!(!project.is_online()); + }); assert!(client_b .user_store .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); @@ -666,7 +676,7 @@ async fn test_offline_projects( // Build another project using a directory which was previously part of // an online project. Restore the project's state from the host's database. - let project2 = cx_a.update(|cx| { + let project2_a = cx_a.update(|cx| { Project::local( false, client_a.client.clone(), @@ -677,21 +687,21 @@ async fn test_offline_projects( cx, ) }); - project2 + project2_a .update(cx_a, |p, cx| { p.find_or_create_local_worktree("/code/crate3", true, cx) }) .await .unwrap(); - project2 + project2_a .update(cx_a, |project, cx| project.restore_state(cx)) .await .unwrap(); // This project is now online, because its directory was previously online. - project2.read_with(cx_a, |project, _| assert!(project.is_online())); + project2_a.read_with(cx_a, |project, _| assert!(project.is_online())); deterministic.run_until_parked(); - let project2_id = project2.read_with(cx_a, |p, _| p.remote_id()).unwrap(); + let project2_id = project2_a.read_with(cx_a, |p, _| p.remote_id()).unwrap(); client_b.user_store.read_with(cx_b, |store, _| { assert_eq!( store.contacts()[0].projects, @@ -714,6 +724,41 @@ async fn test_offline_projects( ); }); + let project2_b = client_b.build_remote_project(&project2_a, cx_a, cx_b).await; + let project2_c = cx_c.foreground().spawn(Project::remote( + project2_id, + client_c.client.clone(), + client_c.user_store.clone(), + client_c.project_store.clone(), + client_c.language_registry.clone(), + FakeFs::new(cx_c.background()), + cx_c.to_async(), + )); + deterministic.run_until_parked(); + + // Taking a project offline unshares the project, rejects any pending join request and + // disconnects existing guests. + project2_a.update(cx_a, |project, cx| project.set_online(false, cx)); + deterministic.run_until_parked(); + project2_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); + project2_b.read_with(cx_b, |project, _| assert!(project.is_read_only())); + project2_c.await.unwrap_err(); + + client_b.user_store.read_with(cx_b, |store, _| { + assert_eq!( + store.contacts()[0].projects, + &[ProjectMetadata { + id: project_id, + visible_worktree_root_names: vec![ + "crate1".into(), + "crate2".into(), + "crate3".into() + ], + guests: Default::default(), + },] + ); + }); + cx_a.update(|cx| { drop(subscriptions); drop(view); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index b7b0e00f2dfb0e0c79dca4b1cec8467f59f22ad3..4944761d58bd6bb1fb330f6fc7a5ecbf3cf62b7b 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -159,6 +159,7 @@ impl Server { .add_message_handler(Server::update_project) .add_message_handler(Server::register_project_activity) .add_request_handler(Server::update_worktree) + .add_message_handler(Server::update_worktree_extensions) .add_message_handler(Server::start_language_server) .add_message_handler(Server::update_language_server) .add_message_handler(Server::update_diagnostic_summary) @@ -327,7 +328,7 @@ impl Server { let period_end = OffsetDateTime::now_utc(); this.app_state .db - .record_project_activity(period_start..period_end, &active_projects) + .record_user_activity(period_start..period_end, &active_projects) .await .trace_err(); period_start = period_end; @@ -600,9 +601,11 @@ impl Server { .await .user_id_for_connection(request.sender_id)?; let project_id = self.app_state.db.register_project(user_id).await?; - self.store() - .await - .register_project(request.sender_id, project_id)?; + self.store().await.register_project( + request.sender_id, + project_id, + request.payload.online, + )?; response.send(proto::RegisterProjectResponse { project_id: project_id.to_proto(), @@ -948,12 +951,53 @@ impl Server { let guest_connection_ids = state .read_project(project_id, request.sender_id)? .guest_connection_ids(); - state.update_project(project_id, &request.payload.worktrees, request.sender_id)?; - broadcast(request.sender_id, guest_connection_ids, |connection_id| { - self.peer - .forward_send(request.sender_id, connection_id, request.payload.clone()) - }); + let unshared_project = state.update_project( + project_id, + &request.payload.worktrees, + request.payload.online, + request.sender_id, + )?; + + if let Some(unshared_project) = unshared_project { + broadcast( + request.sender_id, + unshared_project.guests.keys().copied(), + |conn_id| { + self.peer.send( + conn_id, + proto::UnregisterProject { + project_id: project_id.to_proto(), + }, + ) + }, + ); + for (_, receipts) in unshared_project.pending_join_requests { + for receipt in receipts { + self.peer.respond( + receipt, + proto::JoinProjectResponse { + variant: Some(proto::join_project_response::Variant::Decline( + proto::join_project_response::Decline { + reason: + proto::join_project_response::decline::Reason::Closed + as i32, + }, + )), + }, + )?; + } + } + } else { + broadcast(request.sender_id, guest_connection_ids, |connection_id| { + self.peer.forward_send( + request.sender_id, + connection_id, + request.payload.clone(), + ) + }); + } }; + self.update_user_contacts(user_id).await?; Ok(()) } @@ -976,9 +1020,9 @@ impl Server { ) -> Result<()> { let project_id = ProjectId::from_proto(request.payload.project_id); let worktree_id = request.payload.worktree_id; - let (connection_ids, metadata_changed, extension_counts) = { + let (connection_ids, metadata_changed) = { let mut store = self.store().await; - let (connection_ids, metadata_changed, extension_counts) = store.update_worktree( + let (connection_ids, metadata_changed) = store.update_worktree( request.sender_id, project_id, worktree_id, @@ -988,12 +1032,8 @@ impl Server { request.payload.scan_id, request.payload.is_last_update, )?; - (connection_ids, metadata_changed, extension_counts.clone()) + (connection_ids, metadata_changed) }; - self.app_state - .db - .update_worktree_extensions(project_id, worktree_id, extension_counts) - .await?; broadcast(request.sender_id, connection_ids, |connection_id| { self.peer @@ -1010,6 +1050,25 @@ impl Server { Ok(()) } + async fn update_worktree_extensions( + self: Arc, + request: TypedEnvelope, + ) -> Result<()> { + let project_id = ProjectId::from_proto(request.payload.project_id); + let worktree_id = request.payload.worktree_id; + let extensions = request + .payload + .extensions + .into_iter() + .zip(request.payload.counts) + .collect(); + self.app_state + .db + .update_worktree_extensions(project_id, worktree_id, extensions) + .await?; + Ok(()) + } + async fn update_diagnostic_summary( self: Arc, request: TypedEnvelope, diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 1d36e971e2db1d9160c12c7c3cb538df0e900814..f5cc87cca893ffdce650c7bef2f756acef2d5b5d 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -3,12 +3,7 @@ use anyhow::{anyhow, Result}; use collections::{btree_map, hash_map::Entry, BTreeMap, BTreeSet, HashMap, HashSet}; use rpc::{proto, ConnectionId, Receipt}; use serde::Serialize; -use std::{ - mem, - path::{Path, PathBuf}, - str, - time::Duration, -}; +use std::{mem, path::PathBuf, str, time::Duration}; use time::OffsetDateTime; use tracing::instrument; @@ -32,6 +27,7 @@ struct ConnectionState { #[derive(Serialize)] pub struct Project { + pub online: bool, pub host_connection_id: ConnectionId, pub host: Collaborator, pub guests: HashMap, @@ -58,8 +54,6 @@ pub struct Worktree { #[serde(skip)] pub entries: BTreeMap, #[serde(skip)] - pub extension_counts: HashMap, - #[serde(skip)] pub diagnostic_summaries: BTreeMap, pub scan_id: u64, pub is_complete: bool, @@ -89,6 +83,11 @@ pub struct LeftProject { pub unshare: bool, } +pub struct UnsharedProject { + pub guests: HashMap, + pub pending_join_requests: HashMap>>, +} + #[derive(Copy, Clone)] pub struct Metrics { pub connections: usize, @@ -298,7 +297,7 @@ impl Store { let mut metadata = Vec::new(); for project_id in project_ids { if let Some(project) = self.projects.get(&project_id) { - if project.host.user_id == user_id { + if project.host.user_id == user_id && project.online { metadata.push(proto::ProjectMetadata { id: project_id.to_proto(), visible_worktree_root_names: project @@ -324,6 +323,7 @@ impl Store { &mut self, host_connection_id: ConnectionId, project_id: ProjectId, + online: bool, ) -> Result<()> { let connection = self .connections @@ -333,6 +333,7 @@ impl Store { self.projects.insert( project_id, Project { + online, host_connection_id, host: Collaborator { user_id: connection.user_id, @@ -354,8 +355,9 @@ impl Store { &mut self, project_id: ProjectId, worktrees: &[proto::WorktreeMetadata], + online: bool, connection_id: ConnectionId, - ) -> Result<()> { + ) -> Result> { let project = self .projects .get_mut(&project_id) @@ -376,7 +378,33 @@ impl Store { ); } } - Ok(()) + + if online != project.online { + project.online = online; + if project.online { + Ok(None) + } else { + for connection_id in project.guest_connection_ids() { + if let Some(connection) = self.connections.get_mut(&connection_id) { + connection.projects.remove(&project_id); + } + } + + project.active_replica_ids.clear(); + project.language_servers.clear(); + for worktree in project.worktrees.values_mut() { + worktree.diagnostic_summaries.clear(); + worktree.entries.clear(); + } + + Ok(Some(UnsharedProject { + guests: mem::take(&mut project.guests), + pending_join_requests: mem::take(&mut project.join_requests), + })) + } + } else { + Ok(None) + } } else { Err(anyhow!("no such project"))? } @@ -482,13 +510,17 @@ impl Store { .projects .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; - connection.requested_projects.insert(project_id); - project - .join_requests - .entry(requester_id) - .or_default() - .push(receipt); - Ok(()) + if project.online { + connection.requested_projects.insert(project_id); + project + .join_requests + .entry(requester_id) + .or_default() + .push(receipt); + Ok(()) + } else { + Err(anyhow!("no such project")) + } } pub fn deny_join_project_request( @@ -593,7 +625,6 @@ impl Store { for worktree in project.worktrees.values_mut() { worktree.diagnostic_summaries.clear(); worktree.entries.clear(); - worktree.extension_counts.clear(); } } @@ -617,54 +648,28 @@ impl Store { updated_entries: &[proto::Entry], scan_id: u64, is_last_update: bool, - ) -> Result<(Vec, bool, HashMap)> { + ) -> Result<(Vec, bool)> { let project = self.write_project(project_id, connection_id)?; + if !project.online { + return Err(anyhow!("project is not online")); + } + let connection_ids = project.connection_ids(); let mut worktree = project.worktrees.entry(worktree_id).or_default(); let metadata_changed = worktree_root_name != worktree.root_name; worktree.root_name = worktree_root_name.to_string(); for entry_id in removed_entries { - if let Some(entry) = worktree.entries.remove(&entry_id) { - if !entry.is_ignored { - if let Some(extension) = extension_for_entry(&entry) { - if let Some(count) = worktree.extension_counts.get_mut(extension) { - *count = count.saturating_sub(1); - } - } - } - } + worktree.entries.remove(&entry_id); } for entry in updated_entries { - if let Some(old_entry) = worktree.entries.insert(entry.id, entry.clone()) { - if !old_entry.is_ignored { - if let Some(extension) = extension_for_entry(&old_entry) { - if let Some(count) = worktree.extension_counts.get_mut(extension) { - *count = count.saturating_sub(1); - } - } - } - } - - if !entry.is_ignored { - if let Some(extension) = extension_for_entry(&entry) { - if let Some(count) = worktree.extension_counts.get_mut(extension) { - *count += 1; - } else { - worktree.extension_counts.insert(extension.into(), 1); - } - } - } + worktree.entries.insert(entry.id, entry.clone()); } worktree.scan_id = scan_id; worktree.is_complete = is_last_update; - Ok(( - connection_ids, - metadata_changed, - worktree.extension_counts.clone(), - )) + Ok((connection_ids, metadata_changed)) } pub fn project_connection_ids( @@ -853,11 +858,3 @@ impl Channel { self.connection_ids.iter().copied().collect() } } - -fn extension_for_entry(entry: &proto::Entry) -> Option<&str> { - str::from_utf8(&entry.path) - .ok() - .map(Path::new) - .and_then(|p| p.extension()) - .and_then(|e| e.to_str()) -} diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 8ffbd17a1d3b000ff7115c4ee9905cdb28fa482f..f4010a12785d4bc1ff153af9b7fa3622e3cefcd8 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1263,6 +1263,13 @@ mod tests { .detach(); }); + let request = server.receive::().await.unwrap(); + server + .respond( + request.receipt(), + proto::RegisterProjectResponse { project_id: 200 }, + ) + .await; let get_users_request = server.receive::().await.unwrap(); server .respond( @@ -1340,6 +1347,19 @@ mod tests { ..Default::default() }); + assert_eq!( + server + .receive::() + .await + .unwrap() + .payload, + proto::UpdateProject { + project_id: 200, + online: false, + worktrees: vec![] + }, + ); + cx.foreground().run_until_parked(); assert_eq!( cx.read(|cx| render_to_strings(&panel, cx)), @@ -1383,36 +1403,6 @@ mod tests { ] ); - // The server responds, assigning the project a remote id. It still appears - // as loading, because the server hasn't yet sent out the updated contact - // state for the current user. - let request = server.receive::().await.unwrap(); - server - .respond( - request.receipt(), - proto::RegisterProjectResponse { project_id: 200 }, - ) - .await; - cx.foreground().run_until_parked(); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Requests", - " incoming user_one", - " outgoing user_two", - "v Online", - " the_current_user", - " dir3", - " 🔒 private_dir (going online...)", - " user_four", - " dir2", - " user_three", - " dir1", - "v Offline", - " user_five", - ] - ); - // The server receives the project's metadata and updates the contact metadata // for the current user. Now the project appears as online. assert_eq!( @@ -1420,14 +1410,22 @@ mod tests { .receive::() .await .unwrap() - .payload - .worktrees, - &[proto::WorktreeMetadata { - id: worktree_id, - root_name: "private_dir".to_string(), - visible: true, - }], + .payload, + proto::UpdateProject { + project_id: 200, + online: true, + worktrees: vec![proto::WorktreeMetadata { + id: worktree_id, + root_name: "private_dir".to_string(), + visible: true, + }] + }, ); + server + .receive::() + .await + .unwrap(); + server.send(proto::UpdateContacts { contacts: vec![proto::Contact { user_id: current_user_id, @@ -1492,7 +1490,19 @@ mod tests { // The server receives the unregister request and updates the contact // metadata for the current user. The project is now offline. - let request = server.receive::().await.unwrap(); + assert_eq!( + server + .receive::() + .await + .unwrap() + .payload, + proto::UpdateProject { + project_id: 200, + online: false, + worktrees: vec![] + }, + ); + server.send(proto::UpdateContacts { contacts: vec![proto::Contact { user_id: current_user_id, @@ -1526,28 +1536,6 @@ mod tests { ] ); - // The server responds to the unregister request. - server.respond(request.receipt(), proto::Ack {}).await; - cx.foreground().run_until_parked(); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Requests", - " incoming user_one", - " outgoing user_two", - "v Online", - " the_current_user", - " dir3", - " 🔒 private_dir", - " user_four", - " dir2", - " user_three", - " dir1", - "v Offline", - " user_five", - ] - ); - panel.update(cx, |panel, cx| { panel .filter_editor diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 218bf089495c69f5f5f4299e7f5b4332a59c396a..ad38520032560b5b0901eb2ce7702632191f1a6b 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -592,7 +592,7 @@ impl AsyncAppContext { self.0.borrow().foreground.spawn(f(self.clone())) } - pub fn read T>(&mut self, callback: F) -> T { + pub fn read T>(&self, callback: F) -> T { callback(self.0.borrow().as_ref()) } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 5897b5fcb6a401e4eadf0d8338c44e818eaef0cd..212edb9219e3eae077fb2adcef5aec38bb2080cd 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -890,7 +890,7 @@ impl LspCommand for GetHover { message: Option, _: ModelHandle, buffer: ModelHandle, - mut cx: AsyncAppContext, + cx: AsyncAppContext, ) -> Result { Ok(message.and_then(|hover| { let range = hover.range.map(|range| { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d07bcdb9640b891a0ee335a709bc33cb1d49b008..e8ee4810a39613d620d0722e024df3beb707900d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -148,13 +148,14 @@ enum ProjectClientState { remote_id_rx: watch::Receiver>, online_tx: watch::Sender, online_rx: watch::Receiver, - _maintain_remote_id_task: Task>, + _maintain_remote_id: Task>, + _maintain_online_status: Task>, }, Remote { sharing_has_stopped: bool, remote_id: u64, replica_id: ReplicaId, - _detect_unshare_task: Task>, + _detect_unshare: Task>, }, } @@ -401,17 +402,13 @@ impl Project { cx: &mut MutableAppContext, ) -> ModelHandle { cx.add_model(|cx: &mut ModelContext| { - let (online_tx, online_rx) = watch::channel_with(online); let (remote_id_tx, remote_id_rx) = watch::channel(); - let _maintain_remote_id_task = cx.spawn_weak({ - let status_rx = client.clone().status(); - let online_rx = online_rx.clone(); + let _maintain_remote_id = cx.spawn_weak({ + let mut status_rx = client.clone().status(); move |this, mut cx| async move { - let mut stream = Stream::map(status_rx.clone(), drop) - .merge(Stream::map(online_rx.clone(), drop)); - while stream.recv().await.is_some() { + while let Some(status) = status_rx.recv().await { let this = this.upgrade(&cx)?; - if status_rx.borrow().is_connected() && *online_rx.borrow() { + if status.is_connected() { this.update(&mut cx, |this, cx| this.register(cx)) .await .log_err()?; @@ -425,6 +422,23 @@ impl Project { } }); + let (online_tx, online_rx) = watch::channel_with(online); + let _maintain_online_status = cx.spawn_weak({ + let mut online_rx = online_rx.clone(); + move |this, mut cx| async move { + while let Some(online) = online_rx.recv().await { + let this = this.upgrade(&cx)?; + this.update(&mut cx, |this, cx| { + if !online { + this.unshared(cx); + } + this.metadata_changed(false, cx) + }); + } + None + } + }); + let handle = cx.weak_handle(); project_store.update(cx, |store, cx| store.add_project(handle, cx)); @@ -443,7 +457,8 @@ impl Project { remote_id_rx, online_tx, online_rx, - _maintain_remote_id_task, + _maintain_remote_id, + _maintain_online_status, }, opened_buffer: (Rc::new(RefCell::new(opened_buffer_tx)), opened_buffer_rx), client_subscriptions: Vec::new(), @@ -538,7 +553,7 @@ impl Project { sharing_has_stopped: false, remote_id, replica_id, - _detect_unshare_task: cx.spawn_weak(move |this, mut cx| { + _detect_unshare: cx.spawn_weak(move |this, mut cx| { async move { let mut status = client.status(); let is_connected = @@ -812,13 +827,11 @@ impl Project { &self.fs } - pub fn set_online(&mut self, online: bool, cx: &mut ModelContext) { + pub fn set_online(&mut self, online: bool, _: &mut ModelContext) { if let ProjectClientState::Local { online_tx, .. } = &mut self.client_state { let mut online_tx = online_tx.borrow_mut(); if *online_tx != online { *online_tx = online; - drop(online_tx); - self.metadata_changed(true, cx); } } } @@ -869,27 +882,36 @@ impl Project { } fn register(&mut self, cx: &mut ModelContext) -> Task> { - if let ProjectClientState::Local { remote_id_rx, .. } = &self.client_state { + if let ProjectClientState::Local { + remote_id_rx, + online_rx, + .. + } = &self.client_state + { if remote_id_rx.borrow().is_some() { return Task::ready(Ok(())); } - } - let response = self.client.request(proto::RegisterProject {}); - cx.spawn(|this, mut cx| async move { - let remote_id = response.await?.project_id; - this.update(&mut cx, |this, cx| { - if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state { - *remote_id_tx.borrow_mut() = Some(remote_id); - } + let response = self.client.request(proto::RegisterProject { + online: *online_rx.borrow(), + }); + cx.spawn(|this, mut cx| async move { + let remote_id = response.await?.project_id; + this.update(&mut cx, |this, cx| { + if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state { + *remote_id_tx.borrow_mut() = Some(remote_id); + } - this.metadata_changed(false, cx); - cx.emit(Event::RemoteIdChanged(Some(remote_id))); - this.client_subscriptions - .push(this.client.add_model_for_remote_entity(remote_id, cx)); - Ok(()) + this.metadata_changed(false, cx); + cx.emit(Event::RemoteIdChanged(Some(remote_id))); + this.client_subscriptions + .push(this.client.add_model_for_remote_entity(remote_id, cx)); + Ok(()) + }) }) - }) + } else { + Task::ready(Err(anyhow!("can't register a remote project"))) + } } pub fn remote_id(&self) -> Option { @@ -953,21 +975,52 @@ impl Project { .. } = &self.client_state { - if let (Some(project_id), true) = (*remote_id_rx.borrow(), *online_rx.borrow()) { + // Broadcast worktrees only if the project is online. + let worktrees = if *online_rx.borrow() { + self.worktrees + .iter() + .filter_map(|worktree| { + worktree + .upgrade(&cx) + .map(|worktree| worktree.read(cx).as_local().unwrap().metadata_proto()) + }) + .collect() + } else { + Default::default() + }; + if let Some(project_id) = *remote_id_rx.borrow() { + let online = *online_rx.borrow(); self.client .send(proto::UpdateProject { project_id, - worktrees: self - .worktrees - .iter() - .filter_map(|worktree| { - worktree.upgrade(&cx).map(|worktree| { - worktree.read(cx).as_local().unwrap().metadata_proto() - }) - }) - .collect(), + worktrees, + online, }) .log_err(); + + if online { + let worktrees = self.visible_worktrees(cx).collect::>(); + let scans_complete = + futures::future::join_all(worktrees.iter().filter_map(|worktree| { + Some(worktree.read(cx).as_local()?.scan_complete()) + })); + + let worktrees = worktrees.into_iter().map(|handle| handle.downgrade()); + cx.spawn_weak(move |_, cx| async move { + scans_complete.await; + cx.read(|cx| { + for worktree in worktrees { + if let Some(worktree) = worktree + .upgrade(cx) + .and_then(|worktree| worktree.read(cx).as_local()) + { + worktree.send_extension_counts(project_id); + } + } + }) + }) + .detach(); + } } self.project_store.update(cx, |_, cx| cx.notify()); @@ -1232,6 +1285,10 @@ impl Project { } fn share(&mut self, cx: &mut ModelContext) -> Task> { + if !self.is_online() { + return Task::ready(Err(anyhow!("can't share an offline project"))); + } + let project_id; if let ProjectClientState::Local { remote_id_rx, @@ -1347,7 +1404,11 @@ impl Project { cx: &mut ModelContext, ) { if let Some(project_id) = self.remote_id() { - let share = self.share(cx); + let share = if self.is_online() && allow { + Some(self.share(cx)) + } else { + None + }; let client = self.client.clone(); cx.foreground() .spawn(async move { @@ -1356,7 +1417,9 @@ impl Project { project_id, allow, })?; - share.await?; + if let Some(share) = share { + share.await?; + } anyhow::Ok(()) }) .detach_and_log_err(cx); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 435cf59057827d9c63c197eeb1086257aef4135e..b472a28771dc6ac93d7378122874741a461e38e6 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -106,6 +106,7 @@ pub struct LocalSnapshot { removed_entry_ids: HashMap, next_entry_id: Arc, snapshot: Snapshot, + extension_counts: HashMap, } impl Deref for LocalSnapshot { @@ -381,6 +382,7 @@ impl LocalWorktree { scan_id: 0, is_complete: true, }, + extension_counts: Default::default(), }; if let Some(metadata) = metadata { let entry = Entry::new( @@ -916,6 +918,25 @@ impl LocalWorktree { pub fn is_shared(&self) -> bool { self.share.is_some() } + + pub fn send_extension_counts(&self, project_id: u64) { + let mut extensions = Vec::new(); + let mut counts = Vec::new(); + + for (extension, count) in self.extension_counts() { + extensions.push(extension.to_string_lossy().to_string()); + counts.push(*count as u32); + } + + self.client + .send(proto::UpdateWorktreeExtensions { + project_id, + worktree_id: self.id().to_proto(), + extensions, + counts, + }) + .log_err(); + } } impl RemoteWorktree { @@ -1221,6 +1242,10 @@ impl LocalSnapshot { &self.abs_path } + pub fn extension_counts(&self) -> &HashMap { + &self.extension_counts + } + #[cfg(test)] pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree { let root_name = self.root_name.clone(); @@ -1324,7 +1349,7 @@ impl LocalSnapshot { self.reuse_entry_id(&mut entry); self.entries_by_path.insert_or_replace(entry.clone(), &()); let scan_id = self.scan_id; - self.entries_by_id.insert_or_replace( + let removed_entry = self.entries_by_id.insert_or_replace( PathEntry { id: entry.id, path: entry.path.clone(), @@ -1333,6 +1358,12 @@ impl LocalSnapshot { }, &(), ); + + if let Some(removed_entry) = removed_entry { + self.dec_extension_count(&removed_entry.path, removed_entry.is_ignored); + } + self.inc_extension_count(&entry.path, entry.is_ignored); + entry } @@ -1368,6 +1399,7 @@ impl LocalSnapshot { for mut entry in entries { self.reuse_entry_id(&mut entry); + self.inc_extension_count(&entry.path, entry.is_ignored); entries_by_id_edits.push(Edit::Insert(PathEntry { id: entry.id, path: entry.path.clone(), @@ -1378,7 +1410,33 @@ impl LocalSnapshot { } self.entries_by_path.edit(entries_by_path_edits, &()); - self.entries_by_id.edit(entries_by_id_edits, &()); + let removed_entries = self.entries_by_id.edit(entries_by_id_edits, &()); + + for removed_entry in removed_entries { + self.dec_extension_count(&removed_entry.path, removed_entry.is_ignored); + } + } + + fn inc_extension_count(&mut self, path: &Path, ignored: bool) { + if !ignored { + if let Some(extension) = path.extension() { + if let Some(count) = self.extension_counts.get_mut(extension) { + *count += 1; + } else { + self.extension_counts.insert(extension.into(), 1); + } + } + } + } + + fn dec_extension_count(&mut self, path: &Path, ignored: bool) { + if !ignored { + if let Some(extension) = path.extension() { + if let Some(count) = self.extension_counts.get_mut(extension) { + *count -= 1; + } + } + } } fn reuse_entry_id(&mut self, entry: &mut Entry) { @@ -1408,6 +1466,7 @@ impl LocalSnapshot { .or_insert(entry.id); *removed_entry_id = cmp::max(*removed_entry_id, entry.id); entries_by_id_edits.push(Edit::Remove(entry.id)); + self.dec_extension_count(&entry.path, entry.is_ignored); } self.entries_by_id.edit(entries_by_id_edits, &()); @@ -2837,6 +2896,7 @@ mod tests { scan_id: 0, is_complete: true, }, + extension_counts: Default::default(), }; initial_snapshot.insert_entry( Entry::new( @@ -3116,6 +3176,15 @@ mod tests { .entry_for_path(ignore_parent_path.join(&*GITIGNORE)) .is_some()); } + + // Ensure extension counts are correct. + let mut expected_extension_counts = HashMap::default(); + for extension in self.entries(false).filter_map(|e| e.path.extension()) { + *expected_extension_counts + .entry(extension.into()) + .or_insert(0) += 1; + } + assert_eq!(self.extension_counts, expected_extension_counts); } fn to_vec(&self, include_ignored: bool) -> Vec<(&Path, u64, bool)> { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index e3ca60c2519980d8771d5205517962c904ebbc72..6325e5107d8e128af32a40a7046b67f32369f048 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -38,72 +38,73 @@ message Envelope { UpdateProject update_project = 30; RegisterProjectActivity register_project_activity = 31; UpdateWorktree update_worktree = 32; - - CreateProjectEntry create_project_entry = 33; - RenameProjectEntry rename_project_entry = 34; - CopyProjectEntry copy_project_entry = 35; - DeleteProjectEntry delete_project_entry = 36; - ProjectEntryResponse project_entry_response = 37; - - UpdateDiagnosticSummary update_diagnostic_summary = 38; - StartLanguageServer start_language_server = 39; - UpdateLanguageServer update_language_server = 40; - - OpenBufferById open_buffer_by_id = 41; - OpenBufferByPath open_buffer_by_path = 42; - OpenBufferResponse open_buffer_response = 43; - UpdateBuffer update_buffer = 44; - UpdateBufferFile update_buffer_file = 45; - SaveBuffer save_buffer = 46; - BufferSaved buffer_saved = 47; - BufferReloaded buffer_reloaded = 48; - ReloadBuffers reload_buffers = 49; - ReloadBuffersResponse reload_buffers_response = 50; - FormatBuffers format_buffers = 51; - FormatBuffersResponse format_buffers_response = 52; - GetCompletions get_completions = 53; - GetCompletionsResponse get_completions_response = 54; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 55; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 56; - GetCodeActions get_code_actions = 57; - GetCodeActionsResponse get_code_actions_response = 58; - GetHover get_hover = 59; - GetHoverResponse get_hover_response = 60; - ApplyCodeAction apply_code_action = 61; - ApplyCodeActionResponse apply_code_action_response = 62; - PrepareRename prepare_rename = 63; - PrepareRenameResponse prepare_rename_response = 64; - PerformRename perform_rename = 65; - PerformRenameResponse perform_rename_response = 66; - SearchProject search_project = 67; - SearchProjectResponse search_project_response = 68; - - GetChannels get_channels = 69; - GetChannelsResponse get_channels_response = 70; - JoinChannel join_channel = 71; - JoinChannelResponse join_channel_response = 72; - LeaveChannel leave_channel = 73; - SendChannelMessage send_channel_message = 74; - SendChannelMessageResponse send_channel_message_response = 75; - ChannelMessageSent channel_message_sent = 76; - GetChannelMessages get_channel_messages = 77; - GetChannelMessagesResponse get_channel_messages_response = 78; - - UpdateContacts update_contacts = 79; - UpdateInviteInfo update_invite_info = 80; - ShowContacts show_contacts = 81; - - GetUsers get_users = 82; - FuzzySearchUsers fuzzy_search_users = 83; - UsersResponse users_response = 84; - RequestContact request_contact = 85; - RespondToContactRequest respond_to_contact_request = 86; - RemoveContact remove_contact = 87; - - Follow follow = 88; - FollowResponse follow_response = 89; - UpdateFollowers update_followers = 90; - Unfollow unfollow = 91; + UpdateWorktreeExtensions update_worktree_extensions = 33; + + CreateProjectEntry create_project_entry = 34; + RenameProjectEntry rename_project_entry = 35; + CopyProjectEntry copy_project_entry = 36; + DeleteProjectEntry delete_project_entry = 37; + ProjectEntryResponse project_entry_response = 38; + + UpdateDiagnosticSummary update_diagnostic_summary = 39; + StartLanguageServer start_language_server = 40; + UpdateLanguageServer update_language_server = 41; + + OpenBufferById open_buffer_by_id = 42; + OpenBufferByPath open_buffer_by_path = 43; + OpenBufferResponse open_buffer_response = 44; + UpdateBuffer update_buffer = 45; + UpdateBufferFile update_buffer_file = 46; + SaveBuffer save_buffer = 47; + BufferSaved buffer_saved = 48; + BufferReloaded buffer_reloaded = 49; + ReloadBuffers reload_buffers = 50; + ReloadBuffersResponse reload_buffers_response = 51; + FormatBuffers format_buffers = 52; + FormatBuffersResponse format_buffers_response = 53; + GetCompletions get_completions = 54; + GetCompletionsResponse get_completions_response = 55; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 56; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 57; + GetCodeActions get_code_actions = 58; + GetCodeActionsResponse get_code_actions_response = 59; + GetHover get_hover = 60; + GetHoverResponse get_hover_response = 61; + ApplyCodeAction apply_code_action = 62; + ApplyCodeActionResponse apply_code_action_response = 63; + PrepareRename prepare_rename = 64; + PrepareRenameResponse prepare_rename_response = 65; + PerformRename perform_rename = 66; + PerformRenameResponse perform_rename_response = 67; + SearchProject search_project = 68; + SearchProjectResponse search_project_response = 69; + + GetChannels get_channels = 70; + GetChannelsResponse get_channels_response = 71; + JoinChannel join_channel = 72; + JoinChannelResponse join_channel_response = 73; + LeaveChannel leave_channel = 74; + SendChannelMessage send_channel_message = 75; + SendChannelMessageResponse send_channel_message_response = 76; + ChannelMessageSent channel_message_sent = 77; + GetChannelMessages get_channel_messages = 78; + GetChannelMessagesResponse get_channel_messages_response = 79; + + UpdateContacts update_contacts = 80; + UpdateInviteInfo update_invite_info = 81; + ShowContacts show_contacts = 82; + + GetUsers get_users = 83; + FuzzySearchUsers fuzzy_search_users = 84; + UsersResponse users_response = 85; + RequestContact request_contact = 86; + RespondToContactRequest respond_to_contact_request = 87; + RemoveContact remove_contact = 88; + + Follow follow = 89; + FollowResponse follow_response = 90; + UpdateFollowers update_followers = 91; + Unfollow unfollow = 92; } } @@ -121,7 +122,9 @@ message Test { uint64 id = 1; } -message RegisterProject {} +message RegisterProject { + bool online = 1; +} message RegisterProjectResponse { uint64 project_id = 1; @@ -134,6 +137,7 @@ message UnregisterProject { message UpdateProject { uint64 project_id = 1; repeated WorktreeMetadata worktrees = 2; + bool online = 3; } message RegisterProjectActivity { @@ -198,6 +202,13 @@ message UpdateWorktree { bool is_last_update = 7; } +message UpdateWorktreeExtensions { + uint64 project_id = 1; + uint64 worktree_id = 2; + repeated string extensions = 3; + repeated uint32 counts = 4; +} + message CreateProjectEntry { uint64 project_id = 1; uint64 worktree_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 6200f37cd2da253cb511241c9eed32a2819b50e6..e3844a869276a2fb3ed1a6737c775b909d459c29 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -131,9 +131,9 @@ messages!( (PrepareRename, Background), (PrepareRenameResponse, Background), (ProjectEntryResponse, Foreground), + (ProjectUnshared, Foreground), (RegisterProjectResponse, Foreground), (Ping, Foreground), - (ProjectUnshared, Foreground), (RegisterProject, Foreground), (RegisterProjectActivity, Foreground), (ReloadBuffers, Foreground), @@ -163,6 +163,7 @@ messages!( (UpdateLanguageServer, Foreground), (UpdateProject, Foreground), (UpdateWorktree, Foreground), + (UpdateWorktreeExtensions, Background), ); request_messages!( @@ -255,6 +256,7 @@ entity_messages!( UpdateLanguageServer, UpdateProject, UpdateWorktree, + UpdateWorktreeExtensions, ); entity_messages!(channel_id, ChannelMessageSent); diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index d7d40e81e809040453aef53ea01c9d9554bb71dc..5fff19bb065b95b2da5eb3be25f579fcd41df1fa 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 26; +pub const PROTOCOL_VERSION: u32 = 27;