mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-28 17:43:20 +03:00
Allow following participants who aren't in the same project (#2993)
The goal of this PR is to make Following more intuitive. ### Old Behavior Previously, following was scoped to a project. In order to follow someone in a given window, the window needed to contain a shared project, and the leader needed to be present in the project. Otherwise, following failed. ### New Behavior * You can always follow **any** participant in the current call, in any pane of any window. * When following someone in a project that you're both collaborating in, it works the same as before. * When following someone in an unshared project, or a project that they don't have open, you'll only get updates about the leader's views that don't belong to a project, such as channel notes views. When the leader focuses a file in a different project, you'll get the "follow $LEADER to their active project" indicator ### Todo * [x] Change db schema and RPC protocol so a project id isn't required for following * [x] Change client to allow following into non-project items regardless of the leader's project * [x] Assign colors to users in a way that doesn't require users to be in a shared project.
This commit is contained in:
commit
f011a3df52
2
Procfile
2
Procfile
@ -1,4 +1,4 @@
|
|||||||
web: cd ../zed.dev && PORT=3000 npx vercel dev
|
web: cd ../zed.dev && PORT=3000 npm run dev
|
||||||
collab: cd crates/collab && cargo run serve
|
collab: cd crates/collab && cargo run serve
|
||||||
livekit: livekit-server --dev
|
livekit: livekit-server --dev
|
||||||
postgrest: postgrest crates/collab/admin_api.conf
|
postgrest: postgrest crates/collab/admin_api.conf
|
||||||
|
@ -2,22 +2,23 @@ pub mod call_settings;
|
|||||||
pub mod participant;
|
pub mod participant;
|
||||||
pub mod room;
|
pub mod room;
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use audio::Audio;
|
use audio::Audio;
|
||||||
use call_settings::CallSettings;
|
use call_settings::CallSettings;
|
||||||
use channel::ChannelId;
|
use channel::ChannelId;
|
||||||
use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
|
use client::{
|
||||||
|
proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
|
||||||
|
ZED_ALWAYS_ACTIVE,
|
||||||
|
};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use futures::{future::Shared, FutureExt};
|
use futures::{future::Shared, FutureExt};
|
||||||
use postage::watch;
|
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
|
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
|
||||||
WeakModelHandle,
|
WeakModelHandle,
|
||||||
};
|
};
|
||||||
|
use postage::watch;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use participant::ParticipantLocation;
|
pub use participant::ParticipantLocation;
|
||||||
pub use room::Room;
|
pub use room::Room;
|
||||||
@ -68,6 +69,7 @@ impl ActiveCall {
|
|||||||
location: None,
|
location: None,
|
||||||
pending_invites: Default::default(),
|
pending_invites: Default::default(),
|
||||||
incoming_call: watch::channel(),
|
incoming_call: watch::channel(),
|
||||||
|
|
||||||
_subscriptions: vec![
|
_subscriptions: vec![
|
||||||
client.add_request_handler(cx.handle(), Self::handle_incoming_call),
|
client.add_request_handler(cx.handle(), Self::handle_incoming_call),
|
||||||
client.add_message_handler(cx.handle(), Self::handle_call_canceled),
|
client.add_message_handler(cx.handle(), Self::handle_call_canceled),
|
||||||
@ -348,18 +350,23 @@ impl ActiveCall {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn location(&self) -> Option<&WeakModelHandle<Project>> {
|
||||||
|
self.location.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_location(
|
pub fn set_location(
|
||||||
&mut self,
|
&mut self,
|
||||||
project: Option<&ModelHandle<Project>>,
|
project: Option<&ModelHandle<Project>>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
|
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
|
||||||
self.location = project.map(|project| project.downgrade());
|
self.location = project.map(|project| project.downgrade());
|
||||||
if let Some((room, _)) = self.room.as_ref() {
|
if let Some((room, _)) = self.room.as_ref() {
|
||||||
room.update(cx, |room, cx| room.set_location(project, cx))
|
return room.update(cx, |room, cx| room.set_location(project, cx));
|
||||||
} else {
|
|
||||||
Task::ready(Ok(()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Task::ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
fn set_room(
|
fn set_room(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
use client::ParticipantIndex;
|
||||||
use client::{proto, User};
|
use client::{proto, User};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::WeakModelHandle;
|
use gpui::WeakModelHandle;
|
||||||
@ -43,6 +44,7 @@ pub struct RemoteParticipant {
|
|||||||
pub peer_id: proto::PeerId,
|
pub peer_id: proto::PeerId,
|
||||||
pub projects: Vec<proto::ParticipantProject>,
|
pub projects: Vec<proto::ParticipantProject>,
|
||||||
pub location: ParticipantLocation,
|
pub location: ParticipantLocation,
|
||||||
|
pub participant_index: ParticipantIndex,
|
||||||
pub muted: bool,
|
pub muted: bool,
|
||||||
pub speaking: bool,
|
pub speaking: bool,
|
||||||
pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
|
pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
|
||||||
|
@ -7,7 +7,7 @@ use anyhow::{anyhow, Result};
|
|||||||
use audio::{Audio, Sound};
|
use audio::{Audio, Sound};
|
||||||
use client::{
|
use client::{
|
||||||
proto::{self, PeerId},
|
proto::{self, PeerId},
|
||||||
Client, TypedEnvelope, User, UserStore,
|
Client, ParticipantIndex, TypedEnvelope, User, UserStore,
|
||||||
};
|
};
|
||||||
use collections::{BTreeMap, HashMap, HashSet};
|
use collections::{BTreeMap, HashMap, HashSet};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
@ -714,6 +714,9 @@ impl Room {
|
|||||||
participant.user_id,
|
participant.user_id,
|
||||||
RemoteParticipant {
|
RemoteParticipant {
|
||||||
user: user.clone(),
|
user: user.clone(),
|
||||||
|
participant_index: ParticipantIndex(
|
||||||
|
participant.participant_index,
|
||||||
|
),
|
||||||
peer_id,
|
peer_id,
|
||||||
projects: participant.projects,
|
projects: participant.projects,
|
||||||
location,
|
location,
|
||||||
@ -807,6 +810,15 @@ impl Room {
|
|||||||
let _ = this.leave(cx);
|
let _ = this.leave(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.user_store.update(cx, |user_store, cx| {
|
||||||
|
let participant_indices_by_user_id = this
|
||||||
|
.remote_participants
|
||||||
|
.iter()
|
||||||
|
.map(|(user_id, participant)| (*user_id, participant.participant_index))
|
||||||
|
.collect();
|
||||||
|
user_store.set_participant_indices(participant_indices_by_user_id, cx);
|
||||||
|
});
|
||||||
|
|
||||||
this.check_invariants();
|
this.check_invariants();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
@ -1,22 +1,25 @@
|
|||||||
use crate::Channel;
|
use crate::Channel;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::Client;
|
use client::{Client, Collaborator, UserStore};
|
||||||
|
use collections::HashMap;
|
||||||
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle};
|
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle};
|
||||||
use rpc::{proto, TypedEnvelope};
|
use rpc::{
|
||||||
|
proto::{self, PeerId},
|
||||||
|
TypedEnvelope,
|
||||||
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
pub(crate) fn init(client: &Arc<Client>) {
|
pub(crate) fn init(client: &Arc<Client>) {
|
||||||
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
|
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
|
||||||
client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator);
|
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators);
|
||||||
client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator);
|
|
||||||
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChannelBuffer {
|
pub struct ChannelBuffer {
|
||||||
pub(crate) channel: Arc<Channel>,
|
pub(crate) channel: Arc<Channel>,
|
||||||
connected: bool,
|
connected: bool,
|
||||||
collaborators: Vec<proto::Collaborator>,
|
collaborators: HashMap<PeerId, Collaborator>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
buffer: ModelHandle<language::Buffer>,
|
buffer: ModelHandle<language::Buffer>,
|
||||||
buffer_epoch: u64,
|
buffer_epoch: u64,
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
@ -46,6 +49,7 @@ impl ChannelBuffer {
|
|||||||
pub(crate) async fn new(
|
pub(crate) async fn new(
|
||||||
channel: Arc<Channel>,
|
channel: Arc<Channel>,
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) -> Result<ModelHandle<Self>> {
|
) -> Result<ModelHandle<Self>> {
|
||||||
let response = client
|
let response = client
|
||||||
@ -61,8 +65,6 @@ impl ChannelBuffer {
|
|||||||
.map(language::proto::deserialize_operation)
|
.map(language::proto::deserialize_operation)
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
let collaborators = response.collaborators;
|
|
||||||
|
|
||||||
let buffer = cx.add_model(|_| {
|
let buffer = cx.add_model(|_| {
|
||||||
language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text)
|
language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text)
|
||||||
});
|
});
|
||||||
@ -73,34 +75,45 @@ impl ChannelBuffer {
|
|||||||
anyhow::Ok(cx.add_model(|cx| {
|
anyhow::Ok(cx.add_model(|cx| {
|
||||||
cx.subscribe(&buffer, Self::on_buffer_update).detach();
|
cx.subscribe(&buffer, Self::on_buffer_update).detach();
|
||||||
|
|
||||||
Self {
|
let mut this = Self {
|
||||||
buffer,
|
buffer,
|
||||||
buffer_epoch: response.epoch,
|
buffer_epoch: response.epoch,
|
||||||
client,
|
client,
|
||||||
connected: true,
|
connected: true,
|
||||||
collaborators,
|
collaborators: Default::default(),
|
||||||
channel,
|
channel,
|
||||||
subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
|
subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
|
||||||
}
|
user_store,
|
||||||
|
};
|
||||||
|
this.replace_collaborators(response.collaborators, cx);
|
||||||
|
this
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn user_store(&self) -> &ModelHandle<UserStore> {
|
||||||
|
&self.user_store
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn replace_collaborators(
|
pub(crate) fn replace_collaborators(
|
||||||
&mut self,
|
&mut self,
|
||||||
collaborators: Vec<proto::Collaborator>,
|
collaborators: Vec<proto::Collaborator>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
for old_collaborator in &self.collaborators {
|
let mut new_collaborators = HashMap::default();
|
||||||
if collaborators
|
for collaborator in collaborators {
|
||||||
.iter()
|
if let Ok(collaborator) = Collaborator::from_proto(collaborator) {
|
||||||
.any(|c| c.replica_id == old_collaborator.replica_id)
|
new_collaborators.insert(collaborator.peer_id, collaborator);
|
||||||
{
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (_, old_collaborator) in &self.collaborators {
|
||||||
|
if !new_collaborators.contains_key(&old_collaborator.peer_id) {
|
||||||
self.buffer.update(cx, |buffer, cx| {
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.remove_peer(old_collaborator.replica_id as u16, cx)
|
buffer.remove_peer(old_collaborator.replica_id as u16, cx)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.collaborators = collaborators;
|
self.collaborators = new_collaborators;
|
||||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@ -127,64 +140,14 @@ impl ChannelBuffer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_add_channel_buffer_collaborator(
|
async fn handle_update_channel_buffer_collaborators(
|
||||||
this: ModelHandle<Self>,
|
this: ModelHandle<Self>,
|
||||||
envelope: TypedEnvelope<proto::AddChannelBufferCollaborator>,
|
message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
|
||||||
_: Arc<Client>,
|
|
||||||
mut cx: AsyncAppContext,
|
|
||||||
) -> Result<()> {
|
|
||||||
let collaborator = envelope.payload.collaborator.ok_or_else(|| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"Should have gotten a collaborator in the AddChannelBufferCollaborator message"
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.collaborators.push(collaborator);
|
|
||||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_remove_channel_buffer_collaborator(
|
|
||||||
this: ModelHandle<Self>,
|
|
||||||
message: TypedEnvelope<proto::RemoveChannelBufferCollaborator>,
|
|
||||||
_: Arc<Client>,
|
_: Arc<Client>,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.collaborators.retain(|collaborator| {
|
this.replace_collaborators(message.payload.collaborators, cx);
|
||||||
if collaborator.peer_id == message.payload.peer_id {
|
|
||||||
this.buffer.update(cx, |buffer, cx| {
|
|
||||||
buffer.remove_peer(collaborator.replica_id as u16, cx)
|
|
||||||
});
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_update_channel_buffer_collaborator(
|
|
||||||
this: ModelHandle<Self>,
|
|
||||||
message: TypedEnvelope<proto::UpdateChannelBufferCollaborator>,
|
|
||||||
_: Arc<Client>,
|
|
||||||
mut cx: AsyncAppContext,
|
|
||||||
) -> Result<()> {
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
for collaborator in &mut this.collaborators {
|
|
||||||
if collaborator.peer_id == message.payload.old_peer_id {
|
|
||||||
collaborator.peer_id = message.payload.new_peer_id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
@ -217,7 +180,7 @@ impl ChannelBuffer {
|
|||||||
self.buffer.clone()
|
self.buffer.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn collaborators(&self) -> &[proto::Collaborator] {
|
pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
|
||||||
&self.collaborators
|
&self.collaborators
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,10 +198,11 @@ impl ChannelStore {
|
|||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<ModelHandle<ChannelBuffer>>> {
|
) -> Task<Result<ModelHandle<ChannelBuffer>>> {
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
|
let user_store = self.user_store.clone();
|
||||||
self.open_channel_resource(
|
self.open_channel_resource(
|
||||||
channel_id,
|
channel_id,
|
||||||
|this| &mut this.opened_buffers,
|
|this| &mut this.opened_buffers,
|
||||||
|channel, cx| ChannelBuffer::new(channel, client, cx),
|
|channel, cx| ChannelBuffer::new(channel, client, user_store, cx),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,8 @@ lazy_static! {
|
|||||||
.and_then(|v| v.parse().ok());
|
.and_then(|v| v.parse().ok());
|
||||||
pub static ref ZED_APP_PATH: Option<PathBuf> =
|
pub static ref ZED_APP_PATH: Option<PathBuf> =
|
||||||
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
|
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
|
||||||
|
pub static ref ZED_ALWAYS_ACTIVE: bool =
|
||||||
|
std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| e.len() > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
||||||
|
@ -7,11 +7,15 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
|
|||||||
use postage::{sink::Sink, watch};
|
use postage::{sink::Sink, watch};
|
||||||
use rpc::proto::{RequestMessage, UsersResponse};
|
use rpc::proto::{RequestMessage, UsersResponse};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
|
use text::ReplicaId;
|
||||||
use util::http::HttpClient;
|
use util::http::HttpClient;
|
||||||
use util::TryFutureExt as _;
|
use util::TryFutureExt as _;
|
||||||
|
|
||||||
pub type UserId = u64;
|
pub type UserId = u64;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct ParticipantIndex(pub u32);
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: UserId,
|
pub id: UserId,
|
||||||
@ -19,6 +23,13 @@ pub struct User {
|
|||||||
pub avatar: Option<Arc<ImageData>>,
|
pub avatar: Option<Arc<ImageData>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Collaborator {
|
||||||
|
pub peer_id: proto::PeerId,
|
||||||
|
pub replica_id: ReplicaId,
|
||||||
|
pub user_id: UserId,
|
||||||
|
}
|
||||||
|
|
||||||
impl PartialOrd for User {
|
impl PartialOrd for User {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
Some(self.cmp(other))
|
Some(self.cmp(other))
|
||||||
@ -56,6 +67,7 @@ pub enum ContactRequestStatus {
|
|||||||
|
|
||||||
pub struct UserStore {
|
pub struct UserStore {
|
||||||
users: HashMap<u64, Arc<User>>,
|
users: HashMap<u64, Arc<User>>,
|
||||||
|
participant_indices: HashMap<u64, ParticipantIndex>,
|
||||||
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
|
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
|
||||||
current_user: watch::Receiver<Option<Arc<User>>>,
|
current_user: watch::Receiver<Option<Arc<User>>>,
|
||||||
contacts: Vec<Arc<Contact>>,
|
contacts: Vec<Arc<Contact>>,
|
||||||
@ -81,6 +93,7 @@ pub enum Event {
|
|||||||
kind: ContactEventKind,
|
kind: ContactEventKind,
|
||||||
},
|
},
|
||||||
ShowContacts,
|
ShowContacts,
|
||||||
|
ParticipantIndicesChanged,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@ -118,6 +131,7 @@ impl UserStore {
|
|||||||
current_user: current_user_rx,
|
current_user: current_user_rx,
|
||||||
contacts: Default::default(),
|
contacts: Default::default(),
|
||||||
incoming_contact_requests: Default::default(),
|
incoming_contact_requests: Default::default(),
|
||||||
|
participant_indices: Default::default(),
|
||||||
outgoing_contact_requests: Default::default(),
|
outgoing_contact_requests: Default::default(),
|
||||||
invite_info: None,
|
invite_info: None,
|
||||||
client: Arc::downgrade(&client),
|
client: Arc::downgrade(&client),
|
||||||
@ -641,6 +655,21 @@ impl UserStore {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_participant_indices(
|
||||||
|
&mut self,
|
||||||
|
participant_indices: HashMap<u64, ParticipantIndex>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
if participant_indices != self.participant_indices {
|
||||||
|
self.participant_indices = participant_indices;
|
||||||
|
cx.emit(Event::ParticipantIndicesChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn participant_indices(&self) -> &HashMap<u64, ParticipantIndex> {
|
||||||
|
&self.participant_indices
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
@ -672,6 +701,16 @@ impl Contact {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Collaborator {
|
||||||
|
pub fn from_proto(message: proto::Collaborator) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
|
||||||
|
replica_id: message.replica_id as ReplicaId,
|
||||||
|
user_id: message.user_id as UserId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
|
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
|
||||||
let mut response = http
|
let mut response = http
|
||||||
.get(url, Default::default(), true)
|
.get(url, Default::default(), true)
|
||||||
|
@ -158,7 +158,8 @@ CREATE TABLE "room_participants" (
|
|||||||
"initial_project_id" INTEGER,
|
"initial_project_id" INTEGER,
|
||||||
"calling_user_id" INTEGER NOT NULL REFERENCES users (id),
|
"calling_user_id" INTEGER NOT NULL REFERENCES users (id),
|
||||||
"calling_connection_id" INTEGER NOT NULL,
|
"calling_connection_id" INTEGER NOT NULL,
|
||||||
"calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL
|
"calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL,
|
||||||
|
"participant_index" INTEGER
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
|
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
|
||||||
CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");
|
CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE room_participants ADD COLUMN participant_index INTEGER;
|
@ -510,7 +510,7 @@ pub struct RefreshedRoom {
|
|||||||
|
|
||||||
pub struct RefreshedChannelBuffer {
|
pub struct RefreshedChannelBuffer {
|
||||||
pub connection_ids: Vec<ConnectionId>,
|
pub connection_ids: Vec<ConnectionId>,
|
||||||
pub removed_collaborators: Vec<proto::RemoveChannelBufferCollaborator>,
|
pub collaborators: Vec<proto::Collaborator>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
|
@ -2,6 +2,12 @@ use super::*;
|
|||||||
use prost::Message;
|
use prost::Message;
|
||||||
use text::{EditOperation, UndoOperation};
|
use text::{EditOperation, UndoOperation};
|
||||||
|
|
||||||
|
pub struct LeftChannelBuffer {
|
||||||
|
pub channel_id: ChannelId,
|
||||||
|
pub collaborators: Vec<proto::Collaborator>,
|
||||||
|
pub connections: Vec<ConnectionId>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub async fn join_channel_buffer(
|
pub async fn join_channel_buffer(
|
||||||
&self,
|
&self,
|
||||||
@ -204,23 +210,26 @@ impl Database {
|
|||||||
server_id: ServerId,
|
server_id: ServerId,
|
||||||
) -> Result<RefreshedChannelBuffer> {
|
) -> Result<RefreshedChannelBuffer> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
let collaborators = channel_buffer_collaborator::Entity::find()
|
let db_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)
|
.all(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut connection_ids = Vec::new();
|
let mut connection_ids = Vec::new();
|
||||||
let mut removed_collaborators = Vec::new();
|
let mut collaborators = Vec::new();
|
||||||
let mut collaborator_ids_to_remove = Vec::new();
|
let mut collaborator_ids_to_remove = Vec::new();
|
||||||
for collaborator in &collaborators {
|
for db_collaborator in &db_collaborators {
|
||||||
if !collaborator.connection_lost && collaborator.connection_server_id == server_id {
|
if !db_collaborator.connection_lost
|
||||||
connection_ids.push(collaborator.connection());
|
&& db_collaborator.connection_server_id == server_id
|
||||||
|
{
|
||||||
|
connection_ids.push(db_collaborator.connection());
|
||||||
|
collaborators.push(proto::Collaborator {
|
||||||
|
peer_id: Some(db_collaborator.connection().into()),
|
||||||
|
replica_id: db_collaborator.replica_id.0 as u32,
|
||||||
|
user_id: db_collaborator.user_id.to_proto(),
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
removed_collaborators.push(proto::RemoveChannelBufferCollaborator {
|
collaborator_ids_to_remove.push(db_collaborator.id);
|
||||||
channel_id: channel_id.to_proto(),
|
|
||||||
peer_id: Some(collaborator.connection().into()),
|
|
||||||
});
|
|
||||||
collaborator_ids_to_remove.push(collaborator.id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,7 +240,7 @@ impl Database {
|
|||||||
|
|
||||||
Ok(RefreshedChannelBuffer {
|
Ok(RefreshedChannelBuffer {
|
||||||
connection_ids,
|
connection_ids,
|
||||||
removed_collaborators,
|
collaborators,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@ -241,7 +250,7 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> Result<Vec<ConnectionId>> {
|
) -> Result<LeftChannelBuffer> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
self.leave_channel_buffer_internal(channel_id, connection, &*tx)
|
self.leave_channel_buffer_internal(channel_id, connection, &*tx)
|
||||||
.await
|
.await
|
||||||
@ -275,7 +284,7 @@ impl Database {
|
|||||||
pub async fn leave_channel_buffers(
|
pub async fn leave_channel_buffers(
|
||||||
&self,
|
&self,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> Result<Vec<(ChannelId, Vec<ConnectionId>)>> {
|
) -> Result<Vec<LeftChannelBuffer>> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
|
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
|
||||||
enum QueryChannelIds {
|
enum QueryChannelIds {
|
||||||
@ -294,10 +303,10 @@ impl Database {
|
|||||||
|
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
for channel_id in channel_ids {
|
for channel_id in channel_ids {
|
||||||
let collaborators = self
|
let left_channel_buffer = self
|
||||||
.leave_channel_buffer_internal(channel_id, connection, &*tx)
|
.leave_channel_buffer_internal(channel_id, connection, &*tx)
|
||||||
.await?;
|
.await?;
|
||||||
result.push((channel_id, collaborators));
|
result.push(left_channel_buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@ -310,7 +319,7 @@ impl Database {
|
|||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
tx: &DatabaseTransaction,
|
tx: &DatabaseTransaction,
|
||||||
) -> Result<Vec<ConnectionId>> {
|
) -> Result<LeftChannelBuffer> {
|
||||||
let result = channel_buffer_collaborator::Entity::delete_many()
|
let result = channel_buffer_collaborator::Entity::delete_many()
|
||||||
.filter(
|
.filter(
|
||||||
Condition::all()
|
Condition::all()
|
||||||
@ -327,6 +336,7 @@ impl Database {
|
|||||||
Err(anyhow!("not a collaborator on this project"))?;
|
Err(anyhow!("not a collaborator on this project"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut collaborators = Vec::new();
|
||||||
let mut connections = Vec::new();
|
let mut connections = Vec::new();
|
||||||
let mut rows = channel_buffer_collaborator::Entity::find()
|
let mut rows = channel_buffer_collaborator::Entity::find()
|
||||||
.filter(
|
.filter(
|
||||||
@ -336,19 +346,26 @@ impl Database {
|
|||||||
.await?;
|
.await?;
|
||||||
while let Some(row) = rows.next().await {
|
while let Some(row) = rows.next().await {
|
||||||
let row = row?;
|
let row = row?;
|
||||||
connections.push(ConnectionId {
|
let connection = row.connection();
|
||||||
id: row.connection_id as u32,
|
connections.push(connection);
|
||||||
owner_id: row.connection_server_id.0 as u32,
|
collaborators.push(proto::Collaborator {
|
||||||
|
peer_id: Some(connection.into()),
|
||||||
|
replica_id: row.replica_id.0 as u32,
|
||||||
|
user_id: row.user_id.to_proto(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(rows);
|
drop(rows);
|
||||||
|
|
||||||
if connections.is_empty() {
|
if collaborators.is_empty() {
|
||||||
self.snapshot_channel_buffer(channel_id, &tx).await?;
|
self.snapshot_channel_buffer(channel_id, &tx).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(connections)
|
Ok(LeftChannelBuffer {
|
||||||
|
channel_id,
|
||||||
|
collaborators,
|
||||||
|
connections,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_channel_buffer_collaborators(
|
pub async fn get_channel_buffer_collaborators(
|
||||||
|
@ -738,7 +738,7 @@ impl Database {
|
|||||||
Condition::any()
|
Condition::any()
|
||||||
.add(
|
.add(
|
||||||
Condition::all()
|
Condition::all()
|
||||||
.add(follower::Column::ProjectId.eq(project_id))
|
.add(follower::Column::ProjectId.eq(Some(project_id)))
|
||||||
.add(
|
.add(
|
||||||
follower::Column::LeaderConnectionServerId
|
follower::Column::LeaderConnectionServerId
|
||||||
.eq(connection.owner_id),
|
.eq(connection.owner_id),
|
||||||
@ -747,7 +747,7 @@ impl Database {
|
|||||||
)
|
)
|
||||||
.add(
|
.add(
|
||||||
Condition::all()
|
Condition::all()
|
||||||
.add(follower::Column::ProjectId.eq(project_id))
|
.add(follower::Column::ProjectId.eq(Some(project_id)))
|
||||||
.add(
|
.add(
|
||||||
follower::Column::FollowerConnectionServerId
|
follower::Column::FollowerConnectionServerId
|
||||||
.eq(connection.owner_id),
|
.eq(connection.owner_id),
|
||||||
@ -862,13 +862,46 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn check_room_participants(
|
||||||
|
&self,
|
||||||
|
room_id: RoomId,
|
||||||
|
leader_id: ConnectionId,
|
||||||
|
follower_id: ConnectionId,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
use room_participant::Column;
|
||||||
|
|
||||||
|
let count = room_participant::Entity::find()
|
||||||
|
.filter(
|
||||||
|
Condition::all().add(Column::RoomId.eq(room_id)).add(
|
||||||
|
Condition::any()
|
||||||
|
.add(Column::AnsweringConnectionId.eq(leader_id.id as i32).and(
|
||||||
|
Column::AnsweringConnectionServerId.eq(leader_id.owner_id as i32),
|
||||||
|
))
|
||||||
|
.add(Column::AnsweringConnectionId.eq(follower_id.id as i32).and(
|
||||||
|
Column::AnsweringConnectionServerId.eq(follower_id.owner_id as i32),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.count(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if count < 2 {
|
||||||
|
Err(anyhow!("not room participants"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn follow(
|
pub async fn follow(
|
||||||
&self,
|
&self,
|
||||||
|
room_id: RoomId,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
leader_connection: ConnectionId,
|
leader_connection: ConnectionId,
|
||||||
follower_connection: ConnectionId,
|
follower_connection: ConnectionId,
|
||||||
) -> Result<RoomGuard<proto::Room>> {
|
) -> Result<RoomGuard<proto::Room>> {
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
follower::ActiveModel {
|
follower::ActiveModel {
|
||||||
room_id: ActiveValue::set(room_id),
|
room_id: ActiveValue::set(room_id),
|
||||||
@ -894,15 +927,16 @@ impl Database {
|
|||||||
|
|
||||||
pub async fn unfollow(
|
pub async fn unfollow(
|
||||||
&self,
|
&self,
|
||||||
|
room_id: RoomId,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
leader_connection: ConnectionId,
|
leader_connection: ConnectionId,
|
||||||
follower_connection: ConnectionId,
|
follower_connection: ConnectionId,
|
||||||
) -> Result<RoomGuard<proto::Room>> {
|
) -> Result<RoomGuard<proto::Room>> {
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
follower::Entity::delete_many()
|
follower::Entity::delete_many()
|
||||||
.filter(
|
.filter(
|
||||||
Condition::all()
|
Condition::all()
|
||||||
|
.add(follower::Column::RoomId.eq(room_id))
|
||||||
.add(follower::Column::ProjectId.eq(project_id))
|
.add(follower::Column::ProjectId.eq(project_id))
|
||||||
.add(
|
.add(
|
||||||
follower::Column::LeaderConnectionServerId
|
follower::Column::LeaderConnectionServerId
|
||||||
|
@ -128,6 +128,7 @@ impl Database {
|
|||||||
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||||
connection.owner_id as i32,
|
connection.owner_id as i32,
|
||||||
))),
|
))),
|
||||||
|
participant_index: ActiveValue::set(Some(0)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
.insert(&*tx)
|
.insert(&*tx)
|
||||||
@ -152,6 +153,7 @@ impl Database {
|
|||||||
room_id: ActiveValue::set(room_id),
|
room_id: ActiveValue::set(room_id),
|
||||||
user_id: ActiveValue::set(called_user_id),
|
user_id: ActiveValue::set(called_user_id),
|
||||||
answering_connection_lost: ActiveValue::set(false),
|
answering_connection_lost: ActiveValue::set(false),
|
||||||
|
participant_index: ActiveValue::NotSet,
|
||||||
calling_user_id: ActiveValue::set(calling_user_id),
|
calling_user_id: ActiveValue::set(calling_user_id),
|
||||||
calling_connection_id: ActiveValue::set(calling_connection.id as i32),
|
calling_connection_id: ActiveValue::set(calling_connection.id as i32),
|
||||||
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||||
@ -283,6 +285,26 @@ impl Database {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("no such room"))?;
|
.ok_or_else(|| anyhow!("no such room"))?;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||||
|
enum QueryParticipantIndices {
|
||||||
|
ParticipantIndex,
|
||||||
|
}
|
||||||
|
let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
|
||||||
|
.filter(
|
||||||
|
room_participant::Column::RoomId
|
||||||
|
.eq(room_id)
|
||||||
|
.and(room_participant::Column::ParticipantIndex.is_not_null()),
|
||||||
|
)
|
||||||
|
.select_only()
|
||||||
|
.column(room_participant::Column::ParticipantIndex)
|
||||||
|
.into_values::<_, QueryParticipantIndices>()
|
||||||
|
.all(&*tx)
|
||||||
|
.await?;
|
||||||
|
let mut participant_index = 0;
|
||||||
|
while existing_participant_indices.contains(&participant_index) {
|
||||||
|
participant_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(channel_id) = channel_id {
|
if let Some(channel_id) = channel_id {
|
||||||
self.check_user_is_channel_member(channel_id, user_id, &*tx)
|
self.check_user_is_channel_member(channel_id, user_id, &*tx)
|
||||||
.await?;
|
.await?;
|
||||||
@ -300,6 +322,7 @@ impl Database {
|
|||||||
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||||
connection.owner_id as i32,
|
connection.owner_id as i32,
|
||||||
))),
|
))),
|
||||||
|
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}])
|
}])
|
||||||
.on_conflict(
|
.on_conflict(
|
||||||
@ -308,6 +331,7 @@ impl Database {
|
|||||||
room_participant::Column::AnsweringConnectionId,
|
room_participant::Column::AnsweringConnectionId,
|
||||||
room_participant::Column::AnsweringConnectionServerId,
|
room_participant::Column::AnsweringConnectionServerId,
|
||||||
room_participant::Column::AnsweringConnectionLost,
|
room_participant::Column::AnsweringConnectionLost,
|
||||||
|
room_participant::Column::ParticipantIndex,
|
||||||
])
|
])
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
@ -322,6 +346,7 @@ impl Database {
|
|||||||
.add(room_participant::Column::AnsweringConnectionId.is_null()),
|
.add(room_participant::Column::AnsweringConnectionId.is_null()),
|
||||||
)
|
)
|
||||||
.set(room_participant::ActiveModel {
|
.set(room_participant::ActiveModel {
|
||||||
|
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||||
connection.owner_id as i32,
|
connection.owner_id as i32,
|
||||||
@ -960,6 +985,39 @@ impl Database {
|
|||||||
Ok(room)
|
Ok(room)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn room_connection_ids(
|
||||||
|
&self,
|
||||||
|
room_id: RoomId,
|
||||||
|
connection_id: ConnectionId,
|
||||||
|
) -> Result<RoomGuard<HashSet<ConnectionId>>> {
|
||||||
|
self.room_transaction(room_id, |tx| async move {
|
||||||
|
let mut participants = room_participant::Entity::find()
|
||||||
|
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||||
|
.stream(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut is_participant = false;
|
||||||
|
let mut connection_ids = HashSet::default();
|
||||||
|
while let Some(participant) = participants.next().await {
|
||||||
|
let participant = participant?;
|
||||||
|
if let Some(answering_connection) = participant.answering_connection() {
|
||||||
|
if answering_connection == connection_id {
|
||||||
|
is_participant = true;
|
||||||
|
} else {
|
||||||
|
connection_ids.insert(answering_connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_participant {
|
||||||
|
Err(anyhow!("not a room participant"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(connection_ids)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_channel_room(
|
async fn get_channel_room(
|
||||||
&self,
|
&self,
|
||||||
room_id: RoomId,
|
room_id: RoomId,
|
||||||
@ -978,10 +1036,15 @@ impl Database {
|
|||||||
let mut pending_participants = Vec::new();
|
let mut pending_participants = Vec::new();
|
||||||
while let Some(db_participant) = db_participants.next().await {
|
while let Some(db_participant) = db_participants.next().await {
|
||||||
let db_participant = db_participant?;
|
let db_participant = db_participant?;
|
||||||
if let Some((answering_connection_id, answering_connection_server_id)) = db_participant
|
if let (
|
||||||
.answering_connection_id
|
Some(answering_connection_id),
|
||||||
.zip(db_participant.answering_connection_server_id)
|
Some(answering_connection_server_id),
|
||||||
{
|
Some(participant_index),
|
||||||
|
) = (
|
||||||
|
db_participant.answering_connection_id,
|
||||||
|
db_participant.answering_connection_server_id,
|
||||||
|
db_participant.participant_index,
|
||||||
|
) {
|
||||||
let location = match (
|
let location = match (
|
||||||
db_participant.location_kind,
|
db_participant.location_kind,
|
||||||
db_participant.location_project_id,
|
db_participant.location_project_id,
|
||||||
@ -1012,6 +1075,7 @@ impl Database {
|
|||||||
peer_id: Some(answering_connection.into()),
|
peer_id: Some(answering_connection.into()),
|
||||||
projects: Default::default(),
|
projects: Default::default(),
|
||||||
location: Some(proto::ParticipantLocation { variant: location }),
|
location: Some(proto::ParticipantLocation { variant: location }),
|
||||||
|
participant_index: participant_index as u32,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
|
use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
|
||||||
|
use rpc::ConnectionId;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||||
@ -17,6 +18,16 @@ pub struct Model {
|
|||||||
pub calling_user_id: UserId,
|
pub calling_user_id: UserId,
|
||||||
pub calling_connection_id: i32,
|
pub calling_connection_id: i32,
|
||||||
pub calling_connection_server_id: Option<ServerId>,
|
pub calling_connection_server_id: Option<ServerId>,
|
||||||
|
pub participant_index: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub fn answering_connection(&self) -> Option<ConnectionId> {
|
||||||
|
Some(ConnectionId {
|
||||||
|
owner_id: self.answering_connection_server_id?.0 as u32,
|
||||||
|
id: self.answering_connection_id? as u32,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
@ -134,12 +134,12 @@ async fn test_channel_buffers(db: &Arc<Database>) {
|
|||||||
let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
|
let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
|
||||||
assert_eq!(zed_collaborats, &[a_id, b_id]);
|
assert_eq!(zed_collaborats, &[a_id, b_id]);
|
||||||
|
|
||||||
let collaborators = db
|
let left_buffer = db
|
||||||
.leave_channel_buffer(zed_id, connection_id_b)
|
.leave_channel_buffer(zed_id, connection_id_b)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(collaborators, &[connection_id_a],);
|
assert_eq!(left_buffer.connections, &[connection_id_a],);
|
||||||
|
|
||||||
let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
|
let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
|
||||||
let _ = db
|
let _ = db
|
||||||
|
@ -38,8 +38,8 @@ use lazy_static::lazy_static;
|
|||||||
use prometheus::{register_int_gauge, IntGauge};
|
use prometheus::{register_int_gauge, IntGauge};
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{
|
proto::{
|
||||||
self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, ChannelEdge, EntityMessage,
|
self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage,
|
||||||
EnvelopedMessage, LiveKitConnectionInfo, RequestMessage,
|
LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators,
|
||||||
},
|
},
|
||||||
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
|
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||||
};
|
};
|
||||||
@ -313,9 +313,16 @@ impl Server {
|
|||||||
.trace_err()
|
.trace_err()
|
||||||
{
|
{
|
||||||
for connection_id in refreshed_channel_buffer.connection_ids {
|
for connection_id in refreshed_channel_buffer.connection_ids {
|
||||||
for message in &refreshed_channel_buffer.removed_collaborators {
|
peer.send(
|
||||||
peer.send(connection_id, message.clone()).trace_err();
|
connection_id,
|
||||||
}
|
proto::UpdateChannelBufferCollaborators {
|
||||||
|
channel_id: channel_id.to_proto(),
|
||||||
|
collaborators: refreshed_channel_buffer
|
||||||
|
.collaborators
|
||||||
|
.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.trace_err();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1883,25 +1890,20 @@ async fn follow(
|
|||||||
response: Response<proto::Follow>,
|
response: Response<proto::Follow>,
|
||||||
session: Session,
|
session: Session,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.project_id);
|
let room_id = RoomId::from_proto(request.room_id);
|
||||||
|
let project_id = request.project_id.map(ProjectId::from_proto);
|
||||||
let leader_id = request
|
let leader_id = request
|
||||||
.leader_id
|
.leader_id
|
||||||
.ok_or_else(|| anyhow!("invalid leader id"))?
|
.ok_or_else(|| anyhow!("invalid leader id"))?
|
||||||
.into();
|
.into();
|
||||||
let follower_id = session.connection_id;
|
let follower_id = session.connection_id;
|
||||||
|
|
||||||
{
|
session
|
||||||
let project_connection_ids = session
|
|
||||||
.db()
|
.db()
|
||||||
.await
|
.await
|
||||||
.project_connection_ids(project_id, session.connection_id)
|
.check_room_participants(room_id, leader_id, session.connection_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if !project_connection_ids.contains(&leader_id) {
|
|
||||||
Err(anyhow!("no such peer"))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut response_payload = session
|
let mut response_payload = session
|
||||||
.peer
|
.peer
|
||||||
.forward_request(session.connection_id, leader_id, request)
|
.forward_request(session.connection_id, leader_id, request)
|
||||||
@ -1911,56 +1913,63 @@ async fn follow(
|
|||||||
.retain(|view| view.leader_id != Some(follower_id.into()));
|
.retain(|view| view.leader_id != Some(follower_id.into()));
|
||||||
response.send(response_payload)?;
|
response.send(response_payload)?;
|
||||||
|
|
||||||
|
if let Some(project_id) = project_id {
|
||||||
let room = session
|
let room = session
|
||||||
.db()
|
.db()
|
||||||
.await
|
.await
|
||||||
.follow(project_id, leader_id, follower_id)
|
.follow(room_id, project_id, leader_id, follower_id)
|
||||||
.await?;
|
.await?;
|
||||||
room_updated(&room, &session.peer);
|
room_updated(&room, &session.peer);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
|
async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.project_id);
|
let room_id = RoomId::from_proto(request.room_id);
|
||||||
|
let project_id = request.project_id.map(ProjectId::from_proto);
|
||||||
let leader_id = request
|
let leader_id = request
|
||||||
.leader_id
|
.leader_id
|
||||||
.ok_or_else(|| anyhow!("invalid leader id"))?
|
.ok_or_else(|| anyhow!("invalid leader id"))?
|
||||||
.into();
|
.into();
|
||||||
let follower_id = session.connection_id;
|
let follower_id = session.connection_id;
|
||||||
|
|
||||||
if !session
|
session
|
||||||
.db()
|
.db()
|
||||||
.await
|
.await
|
||||||
.project_connection_ids(project_id, session.connection_id)
|
.check_room_participants(room_id, leader_id, session.connection_id)
|
||||||
.await?
|
.await?;
|
||||||
.contains(&leader_id)
|
|
||||||
{
|
|
||||||
Err(anyhow!("no such peer"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
session
|
session
|
||||||
.peer
|
.peer
|
||||||
.forward_send(session.connection_id, leader_id, request)?;
|
.forward_send(session.connection_id, leader_id, request)?;
|
||||||
|
|
||||||
|
if let Some(project_id) = project_id {
|
||||||
let room = session
|
let room = session
|
||||||
.db()
|
.db()
|
||||||
.await
|
.await
|
||||||
.unfollow(project_id, leader_id, follower_id)
|
.unfollow(room_id, project_id, leader_id, follower_id)
|
||||||
.await?;
|
.await?;
|
||||||
room_updated(&room, &session.peer);
|
room_updated(&room, &session.peer);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> {
|
async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.project_id);
|
let room_id = RoomId::from_proto(request.room_id);
|
||||||
let project_connection_ids = session
|
let database = session.db.lock().await;
|
||||||
.db
|
|
||||||
.lock()
|
let connection_ids = if let Some(project_id) = request.project_id {
|
||||||
.await
|
let project_id = ProjectId::from_proto(project_id);
|
||||||
|
database
|
||||||
.project_connection_ids(project_id, session.connection_id)
|
.project_connection_ids(project_id, session.connection_id)
|
||||||
.await?;
|
.await?
|
||||||
|
} else {
|
||||||
|
database
|
||||||
|
.room_connection_ids(room_id, session.connection_id)
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
let leader_id = request.variant.as_ref().and_then(|variant| match variant {
|
let leader_id = request.variant.as_ref().and_then(|variant| match variant {
|
||||||
proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
|
proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
|
||||||
@ -1969,9 +1978,7 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
|
|||||||
});
|
});
|
||||||
for follower_peer_id in request.follower_ids.iter().copied() {
|
for follower_peer_id in request.follower_ids.iter().copied() {
|
||||||
let follower_connection_id = follower_peer_id.into();
|
let follower_connection_id = follower_peer_id.into();
|
||||||
if project_connection_ids.contains(&follower_connection_id)
|
if Some(follower_peer_id) != leader_id && connection_ids.contains(&follower_connection_id) {
|
||||||
&& Some(follower_peer_id) != leader_id
|
|
||||||
{
|
|
||||||
session.peer.forward_send(
|
session.peer.forward_send(
|
||||||
session.connection_id,
|
session.connection_id,
|
||||||
follower_connection_id,
|
follower_connection_id,
|
||||||
@ -2658,18 +2665,12 @@ async fn join_channel_buffer(
|
|||||||
.join_channel_buffer(channel_id, session.user_id, session.connection_id)
|
.join_channel_buffer(channel_id, session.user_id, session.connection_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let replica_id = open_response.replica_id;
|
|
||||||
let collaborators = open_response.collaborators.clone();
|
let collaborators = open_response.collaborators.clone();
|
||||||
|
|
||||||
response.send(open_response)?;
|
response.send(open_response)?;
|
||||||
|
|
||||||
let update = AddChannelBufferCollaborator {
|
let update = UpdateChannelBufferCollaborators {
|
||||||
channel_id: channel_id.to_proto(),
|
channel_id: channel_id.to_proto(),
|
||||||
collaborator: Some(proto::Collaborator {
|
collaborators: collaborators.clone(),
|
||||||
user_id: session.user_id.to_proto(),
|
|
||||||
peer_id: Some(session.connection_id.into()),
|
|
||||||
replica_id,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
channel_buffer_updated(
|
channel_buffer_updated(
|
||||||
session.connection_id,
|
session.connection_id,
|
||||||
@ -2716,8 +2717,8 @@ async fn rejoin_channel_buffers(
|
|||||||
.rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id)
|
.rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for buffer in &buffers {
|
for rejoined_buffer in &buffers {
|
||||||
let collaborators_to_notify = buffer
|
let collaborators_to_notify = rejoined_buffer
|
||||||
.buffer
|
.buffer
|
||||||
.collaborators
|
.collaborators
|
||||||
.iter()
|
.iter()
|
||||||
@ -2725,10 +2726,9 @@ async fn rejoin_channel_buffers(
|
|||||||
channel_buffer_updated(
|
channel_buffer_updated(
|
||||||
session.connection_id,
|
session.connection_id,
|
||||||
collaborators_to_notify,
|
collaborators_to_notify,
|
||||||
&proto::UpdateChannelBufferCollaborator {
|
&proto::UpdateChannelBufferCollaborators {
|
||||||
channel_id: buffer.buffer.channel_id,
|
channel_id: rejoined_buffer.buffer.channel_id,
|
||||||
old_peer_id: Some(buffer.old_connection_id.into()),
|
collaborators: rejoined_buffer.buffer.collaborators.clone(),
|
||||||
new_peer_id: Some(session.connection_id.into()),
|
|
||||||
},
|
},
|
||||||
&session.peer,
|
&session.peer,
|
||||||
);
|
);
|
||||||
@ -2749,7 +2749,7 @@ async fn leave_channel_buffer(
|
|||||||
let db = session.db().await;
|
let db = session.db().await;
|
||||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||||
|
|
||||||
let collaborators_to_notify = db
|
let left_buffer = db
|
||||||
.leave_channel_buffer(channel_id, session.connection_id)
|
.leave_channel_buffer(channel_id, session.connection_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -2757,10 +2757,10 @@ async fn leave_channel_buffer(
|
|||||||
|
|
||||||
channel_buffer_updated(
|
channel_buffer_updated(
|
||||||
session.connection_id,
|
session.connection_id,
|
||||||
collaborators_to_notify,
|
left_buffer.connections,
|
||||||
&proto::RemoveChannelBufferCollaborator {
|
&proto::UpdateChannelBufferCollaborators {
|
||||||
channel_id: channel_id.to_proto(),
|
channel_id: channel_id.to_proto(),
|
||||||
peer_id: Some(session.connection_id.into()),
|
collaborators: left_buffer.collaborators,
|
||||||
},
|
},
|
||||||
&session.peer,
|
&session.peer,
|
||||||
);
|
);
|
||||||
@ -3235,13 +3235,13 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
|
|||||||
.leave_channel_buffers(session.connection_id)
|
.leave_channel_buffers(session.connection_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for (channel_id, connections) in left_channel_buffers {
|
for left_buffer in left_channel_buffers {
|
||||||
channel_buffer_updated(
|
channel_buffer_updated(
|
||||||
session.connection_id,
|
session.connection_id,
|
||||||
connections,
|
left_buffer.connections,
|
||||||
&proto::RemoveChannelBufferCollaborator {
|
&proto::UpdateChannelBufferCollaborators {
|
||||||
channel_id: channel_id.to_proto(),
|
channel_id: left_buffer.channel_id.to_proto(),
|
||||||
peer_id: Some(session.connection_id.into()),
|
collaborators: left_buffer.collaborators,
|
||||||
},
|
},
|
||||||
&session.peer,
|
&session.peer,
|
||||||
);
|
);
|
||||||
|
@ -4,14 +4,16 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use channel::Channel;
|
use channel::Channel;
|
||||||
use client::UserId;
|
use client::ParticipantIndex;
|
||||||
|
use client::{Collaborator, UserId};
|
||||||
use collab_ui::channel_view::ChannelView;
|
use collab_ui::channel_view::ChannelView;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
|
use editor::{Anchor, Editor, ToOffset};
|
||||||
use futures::future;
|
use futures::future;
|
||||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
|
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
|
||||||
use rpc::{proto, RECEIVE_TIMEOUT};
|
use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::sync::Arc;
|
use std::{ops::Range, sync::Arc};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_core_channel_buffers(
|
async fn test_core_channel_buffers(
|
||||||
@ -100,7 +102,7 @@ async fn test_core_channel_buffers(
|
|||||||
channel_buffer_b.read_with(cx_b, |buffer, _| {
|
channel_buffer_b.read_with(cx_b, |buffer, _| {
|
||||||
assert_collaborators(
|
assert_collaborators(
|
||||||
&buffer.collaborators(),
|
&buffer.collaborators(),
|
||||||
&[client_b.user_id(), client_a.user_id()],
|
&[client_a.user_id(), client_b.user_id()],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -120,10 +122,10 @@ async fn test_core_channel_buffers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_channel_buffer_replica_ids(
|
async fn test_channel_notes_participant_indices(
|
||||||
deterministic: Arc<Deterministic>,
|
deterministic: Arc<Deterministic>,
|
||||||
cx_a: &mut TestAppContext,
|
mut cx_a: &mut TestAppContext,
|
||||||
cx_b: &mut TestAppContext,
|
mut cx_b: &mut TestAppContext,
|
||||||
cx_c: &mut TestAppContext,
|
cx_c: &mut TestAppContext,
|
||||||
) {
|
) {
|
||||||
deterministic.forbid_parking();
|
deterministic.forbid_parking();
|
||||||
@ -132,6 +134,13 @@ async fn test_channel_buffer_replica_ids(
|
|||||||
let client_b = server.create_client(cx_b, "user_b").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_c = server.create_client(cx_c, "user_c").await;
|
||||||
|
|
||||||
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
let active_call_b = cx_b.read(ActiveCall::global);
|
||||||
|
|
||||||
|
cx_a.update(editor::init);
|
||||||
|
cx_b.update(editor::init);
|
||||||
|
cx_c.update(editor::init);
|
||||||
|
|
||||||
let channel_id = server
|
let channel_id = server
|
||||||
.make_channel(
|
.make_channel(
|
||||||
"the-channel",
|
"the-channel",
|
||||||
@ -141,140 +150,173 @@ async fn test_channel_buffer_replica_ids(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let active_call_a = cx_a.read(ActiveCall::global);
|
client_a
|
||||||
let active_call_b = cx_b.read(ActiveCall::global);
|
|
||||||
let active_call_c = cx_c.read(ActiveCall::global);
|
|
||||||
|
|
||||||
// Clients A and B join a channel.
|
|
||||||
active_call_a
|
|
||||||
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
active_call_b
|
|
||||||
.update(cx_b, |call, cx| call.join_channel(channel_id, cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Clients A, B, and C join a channel buffer
|
|
||||||
// C first so that the replica IDs in the project and the channel buffer are different
|
|
||||||
let channel_buffer_c = client_c
|
|
||||||
.channel_store()
|
|
||||||
.update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let channel_buffer_b = client_b
|
|
||||||
.channel_store()
|
|
||||||
.update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let channel_buffer_a = client_a
|
|
||||||
.channel_store()
|
|
||||||
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Client B shares a project
|
|
||||||
client_b
|
|
||||||
.fs()
|
.fs()
|
||||||
.insert_tree("/dir", json!({ "file.txt": "contents" }))
|
.insert_tree("/root", json!({"file.txt": "123"}))
|
||||||
.await;
|
.await;
|
||||||
let (project_b, _) = client_b.build_local_project("/dir", cx_b).await;
|
let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await;
|
||||||
let shared_project_id = active_call_b
|
let project_b = client_b.build_empty_local_project(cx_b);
|
||||||
.update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
|
let project_c = client_c.build_empty_local_project(cx_c);
|
||||||
|
let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||||
|
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||||
|
let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
|
||||||
|
|
||||||
|
// Clients A, B, and C open the channel notes
|
||||||
|
let channel_view_a = cx_a
|
||||||
|
.update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let channel_view_b = cx_b
|
||||||
|
.update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let channel_view_c = cx_c
|
||||||
|
.update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Client A joins the project
|
// Clients A, B, and C all insert and select some text
|
||||||
let project_a = client_a.build_remote_project(shared_project_id, cx_a).await;
|
channel_view_a.update(cx_a, |notes, cx| {
|
||||||
|
notes.editor.update(cx, |editor, cx| {
|
||||||
|
editor.insert("a", cx);
|
||||||
|
editor.change_selections(None, cx, |selections| {
|
||||||
|
selections.select_ranges(vec![0..1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
|
channel_view_b.update(cx_b, |notes, cx| {
|
||||||
// Client C is in a separate project.
|
notes.editor.update(cx, |editor, cx| {
|
||||||
client_c.fs().insert_tree("/dir", json!({})).await;
|
editor.move_down(&Default::default(), cx);
|
||||||
let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await;
|
editor.insert("b", cx);
|
||||||
|
editor.change_selections(None, cx, |selections| {
|
||||||
// Note that each user has a different replica id in the projects vs the
|
selections.select_ranges(vec![1..2]);
|
||||||
// channel buffer.
|
});
|
||||||
channel_buffer_a.read_with(cx_a, |channel_buffer, cx| {
|
});
|
||||||
assert_eq!(project_a.read(cx).replica_id(), 1);
|
});
|
||||||
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2);
|
deterministic.run_until_parked();
|
||||||
|
channel_view_c.update(cx_c, |notes, cx| {
|
||||||
|
notes.editor.update(cx, |editor, cx| {
|
||||||
|
editor.move_down(&Default::default(), cx);
|
||||||
|
editor.insert("c", cx);
|
||||||
|
editor.change_selections(None, cx, |selections| {
|
||||||
|
selections.select_ranges(vec![2..3]);
|
||||||
});
|
});
|
||||||
channel_buffer_b.read_with(cx_b, |channel_buffer, cx| {
|
|
||||||
assert_eq!(project_b.read(cx).replica_id(), 0);
|
|
||||||
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1);
|
|
||||||
});
|
});
|
||||||
channel_buffer_c.read_with(cx_c, |channel_buffer, cx| {
|
|
||||||
// C is not in the project
|
|
||||||
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let channel_window_a =
|
// Client A sees clients B and C without assigned colors, because they aren't
|
||||||
cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx));
|
// in a call together.
|
||||||
let channel_window_b =
|
deterministic.run_until_parked();
|
||||||
cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx));
|
channel_view_a.update(cx_a, |notes, cx| {
|
||||||
let channel_window_c = cx_c.add_window(|cx| {
|
notes.editor.update(cx, |editor, cx| {
|
||||||
ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx)
|
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let channel_view_a = channel_window_a.root(cx_a);
|
// Clients A and B join the same call.
|
||||||
let channel_view_b = channel_window_b.root(cx_b);
|
for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] {
|
||||||
let channel_view_c = channel_window_c.root(cx_c);
|
call.update(*cx, |call, cx| call.join_channel(channel_id, cx))
|
||||||
|
|
||||||
// For clients A and B, the replica ids in the channel buffer are mapped
|
|
||||||
// so that they match the same users' replica ids in their shared project.
|
|
||||||
channel_view_a.read_with(cx_a, |view, cx| {
|
|
||||||
assert_eq!(
|
|
||||||
view.editor.read(cx).replica_id_map().unwrap(),
|
|
||||||
&[(1, 0), (2, 1)].into_iter().collect::<HashMap<_, _>>()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
channel_view_b.read_with(cx_b, |view, cx| {
|
|
||||||
assert_eq!(
|
|
||||||
view.editor.read(cx).replica_id_map().unwrap(),
|
|
||||||
&[(1, 0), (2, 1)].into_iter().collect::<HashMap<u16, u16>>(),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Client C only sees themself, as they're not part of any shared project
|
|
||||||
channel_view_c.read_with(cx_c, |view, cx| {
|
|
||||||
assert_eq!(
|
|
||||||
view.editor.read(cx).replica_id_map().unwrap(),
|
|
||||||
&[(0, 0)].into_iter().collect::<HashMap<u16, u16>>(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Client C joins the project that clients A and B are in.
|
|
||||||
active_call_c
|
|
||||||
.update(cx_c, |call, cx| call.join_channel(channel_id, cx))
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let project_c = client_c.build_remote_project(shared_project_id, cx_c).await;
|
}
|
||||||
deterministic.run_until_parked();
|
|
||||||
project_c.read_with(cx_c, |project, _| {
|
|
||||||
assert_eq!(project.replica_id(), 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
// For clients A and B, client C's replica id in the channel buffer is
|
// Clients A and B see each other with two different assigned colors. Client C
|
||||||
// now mapped to their replica id in the shared project.
|
// still doesn't have a color.
|
||||||
channel_view_a.read_with(cx_a, |view, cx| {
|
deterministic.run_until_parked();
|
||||||
assert_eq!(
|
channel_view_a.update(cx_a, |notes, cx| {
|
||||||
view.editor.read(cx).replica_id_map().unwrap(),
|
notes.editor.update(cx, |editor, cx| {
|
||||||
&[(1, 0), (2, 1), (0, 2)]
|
assert_remote_selections(
|
||||||
.into_iter()
|
editor,
|
||||||
.collect::<HashMap<_, _>>()
|
&[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
channel_view_b.read_with(cx_b, |view, cx| {
|
});
|
||||||
assert_eq!(
|
channel_view_b.update(cx_b, |notes, cx| {
|
||||||
view.editor.read(cx).replica_id_map().unwrap(),
|
notes.editor.update(cx, |editor, cx| {
|
||||||
&[(1, 0), (2, 1), (0, 2)]
|
assert_remote_selections(
|
||||||
.into_iter()
|
editor,
|
||||||
.collect::<HashMap<_, _>>(),
|
&[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
|
||||||
)
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client A shares a project, and client B joins.
|
||||||
|
let project_id = active_call_a
|
||||||
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||||
|
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||||
|
|
||||||
|
// Clients A and B open the same file.
|
||||||
|
let editor_a = workspace_a
|
||||||
|
.update(cx_a, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
let editor_b = workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
editor_a.update(cx_a, |editor, cx| {
|
||||||
|
editor.change_selections(None, cx, |selections| {
|
||||||
|
selections.select_ranges(vec![0..1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
editor_b.update(cx_b, |editor, cx| {
|
||||||
|
editor.change_selections(None, cx, |selections| {
|
||||||
|
selections.select_ranges(vec![2..3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
// Clients A and B see each other with the same colors as in the channel notes.
|
||||||
|
editor_a.update(cx_a, |editor, cx| {
|
||||||
|
assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx);
|
||||||
|
});
|
||||||
|
editor_b.update(cx_b, |editor, cx| {
|
||||||
|
assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_remote_selections(
|
||||||
|
editor: &mut Editor,
|
||||||
|
expected_selections: &[(Option<ParticipantIndex>, Range<usize>)],
|
||||||
|
cx: &mut ViewContext<Editor>,
|
||||||
|
) {
|
||||||
|
let snapshot = editor.snapshot(cx);
|
||||||
|
let range = Anchor::min()..Anchor::max();
|
||||||
|
let remote_selections = snapshot
|
||||||
|
.remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx)
|
||||||
|
.map(|s| {
|
||||||
|
let start = s.selection.start.to_offset(&snapshot.buffer_snapshot);
|
||||||
|
let end = s.selection.end.to_offset(&snapshot.buffer_snapshot);
|
||||||
|
(s.participant_index, start..end)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
remote_selections, expected_selections,
|
||||||
|
"incorrect remote selections"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_reopen_channel_buffer(deterministic: Arc<Deterministic>, cx_a: &mut TestAppContext) {
|
async fn test_multiple_handles_to_channel_buffer(
|
||||||
|
deterministic: Arc<Deterministic>,
|
||||||
|
cx_a: &mut TestAppContext,
|
||||||
|
) {
|
||||||
deterministic.forbid_parking();
|
deterministic.forbid_parking();
|
||||||
let mut server = TestServer::start(&deterministic).await;
|
let mut server = TestServer::start(&deterministic).await;
|
||||||
let client_a = server.create_client(cx_a, "user_a").await;
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
@ -565,26 +607,165 @@ async fn test_channel_buffers_and_server_restarts(
|
|||||||
|
|
||||||
channel_buffer_a.read_with(cx_a, |buffer_a, _| {
|
channel_buffer_a.read_with(cx_a, |buffer_a, _| {
|
||||||
channel_buffer_b.read_with(cx_b, |buffer_b, _| {
|
channel_buffer_b.read_with(cx_b, |buffer_b, _| {
|
||||||
assert_eq!(
|
assert_collaborators(
|
||||||
buffer_a
|
buffer_a.collaborators(),
|
||||||
.collaborators()
|
&[client_a.user_id(), client_b.user_id()],
|
||||||
.iter()
|
|
||||||
.map(|c| c.user_id)
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec![client_a.user_id().unwrap(), client_b.user_id().unwrap()]
|
|
||||||
);
|
);
|
||||||
assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
|
assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[gpui::test(iterations = 10)]
|
||||||
fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option<UserId>]) {
|
async fn test_following_to_channel_notes_without_a_shared_project(
|
||||||
|
deterministic: Arc<Deterministic>,
|
||||||
|
mut cx_a: &mut TestAppContext,
|
||||||
|
mut cx_b: &mut TestAppContext,
|
||||||
|
mut 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;
|
||||||
|
|
||||||
|
cx_a.update(editor::init);
|
||||||
|
cx_b.update(editor::init);
|
||||||
|
cx_c.update(editor::init);
|
||||||
|
cx_a.update(collab_ui::channel_view::init);
|
||||||
|
cx_b.update(collab_ui::channel_view::init);
|
||||||
|
cx_c.update(collab_ui::channel_view::init);
|
||||||
|
|
||||||
|
let channel_1_id = server
|
||||||
|
.make_channel(
|
||||||
|
"channel-1",
|
||||||
|
None,
|
||||||
|
(&client_a, cx_a),
|
||||||
|
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let channel_2_id = server
|
||||||
|
.make_channel(
|
||||||
|
"channel-2",
|
||||||
|
None,
|
||||||
|
(&client_a, cx_a),
|
||||||
|
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Clients A, B, and C join a channel.
|
||||||
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
let active_call_b = cx_b.read(ActiveCall::global);
|
||||||
|
let active_call_c = cx_c.read(ActiveCall::global);
|
||||||
|
for (call, cx) in [
|
||||||
|
(&active_call_a, &mut cx_a),
|
||||||
|
(&active_call_b, &mut cx_b),
|
||||||
|
(&active_call_c, &mut cx_c),
|
||||||
|
] {
|
||||||
|
call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
// Clients A, B, and C all open their own unshared projects.
|
||||||
|
client_a.fs().insert_tree("/a", json!({})).await;
|
||||||
|
client_b.fs().insert_tree("/b", json!({})).await;
|
||||||
|
client_c.fs().insert_tree("/c", json!({})).await;
|
||||||
|
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||||
|
let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
|
||||||
|
let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
|
||||||
|
let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||||
|
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||||
|
let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
|
||||||
|
|
||||||
|
active_call_a
|
||||||
|
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client A opens the notes for channel 1.
|
||||||
|
let channel_view_1_a = cx_a
|
||||||
|
.update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
channel_view_1_a.update(cx_a, |notes, cx| {
|
||||||
|
assert_eq!(notes.channel(cx).name, "channel-1");
|
||||||
|
notes.editor.update(cx, |editor, cx| {
|
||||||
|
editor.insert("Hello from A.", cx);
|
||||||
|
editor.change_selections(None, cx, |selections| {
|
||||||
|
selections.select_ranges(vec![3..4]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client B follows client A.
|
||||||
|
workspace_b
|
||||||
|
.update(cx_b, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.toggle_follow(client_a.peer_id().unwrap(), cx)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client B is taken to the notes for channel 1, with the same
|
||||||
|
// text selected as client A.
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
collaborators
|
workspace.leader_for_pane(workspace.active_pane()),
|
||||||
.into_iter()
|
Some(client_a.peer_id().unwrap())
|
||||||
|
);
|
||||||
|
workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.expect("no active item")
|
||||||
|
.downcast::<ChannelView>()
|
||||||
|
.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");
|
||||||
|
let editor = notes.editor.read(cx);
|
||||||
|
assert_eq!(editor.text(cx), "Hello from A.");
|
||||||
|
assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client A opens the notes for channel 2.
|
||||||
|
let channel_view_2_a = cx_a
|
||||||
|
.update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
channel_view_2_a.read_with(cx_a, |notes, cx| {
|
||||||
|
assert_eq!(notes.channel(cx).name, "channel-2");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Client B is taken to the notes for channel 2.
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
workspace.leader_for_pane(workspace.active_pane()),
|
||||||
|
Some(client_a.peer_id().unwrap())
|
||||||
|
);
|
||||||
|
workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.expect("no active item")
|
||||||
|
.downcast::<ChannelView>()
|
||||||
|
.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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_collaborators(collaborators: &HashMap<PeerId, Collaborator>, ids: &[Option<UserId>]) {
|
||||||
|
let mut user_ids = collaborators
|
||||||
|
.values()
|
||||||
.map(|collaborator| collaborator.user_id)
|
.map(|collaborator| collaborator.user_id)
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>();
|
||||||
|
user_ids.sort();
|
||||||
|
assert_eq!(
|
||||||
|
user_ids,
|
||||||
ids.into_iter().map(|id| id.unwrap()).collect::<Vec<_>>()
|
ids.into_iter().map(|id| id.unwrap()).collect::<Vec<_>>()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6848,9 +6848,9 @@ async fn test_basic_following(
|
|||||||
let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
|
let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
|
||||||
workspace
|
workspace
|
||||||
.active_item(cx)
|
.active_item(cx)
|
||||||
.unwrap()
|
.expect("no active item")
|
||||||
.downcast::<SharedScreen>()
|
.downcast::<SharedScreen>()
|
||||||
.unwrap()
|
.expect("active item isn't a shared screen")
|
||||||
});
|
});
|
||||||
|
|
||||||
// Client B activates Zed again, which causes the previous editor to become focused again.
|
// Client B activates Zed again, which causes the previous editor to become focused again.
|
||||||
|
@ -273,7 +273,7 @@ impl RandomizedTest for RandomChannelBufferTest {
|
|||||||
// channel buffer.
|
// channel buffer.
|
||||||
let collaborators = channel_buffer.collaborators();
|
let collaborators = channel_buffer.collaborators();
|
||||||
let mut user_ids =
|
let mut user_ids =
|
||||||
collaborators.iter().map(|c| c.user_id).collect::<Vec<_>>();
|
collaborators.values().map(|c| c.user_id).collect::<Vec<_>>();
|
||||||
user_ids.sort();
|
user_ids.sort();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
user_ids,
|
user_ids,
|
||||||
|
@ -29,7 +29,7 @@ use std::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use util::http::FakeHttpClient;
|
use util::http::FakeHttpClient;
|
||||||
use workspace::Workspace;
|
use workspace::{Workspace, WorkspaceStore};
|
||||||
|
|
||||||
pub struct TestServer {
|
pub struct TestServer {
|
||||||
pub app_state: Arc<AppState>,
|
pub app_state: Arc<AppState>,
|
||||||
@ -204,13 +204,17 @@ impl TestServer {
|
|||||||
|
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||||
|
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||||
let channel_store =
|
let channel_store =
|
||||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||||
|
let mut language_registry = LanguageRegistry::test();
|
||||||
|
language_registry.set_executor(cx.background());
|
||||||
let app_state = Arc::new(workspace::AppState {
|
let app_state = Arc::new(workspace::AppState {
|
||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
user_store: user_store.clone(),
|
user_store: user_store.clone(),
|
||||||
|
workspace_store,
|
||||||
channel_store: channel_store.clone(),
|
channel_store: channel_store.clone(),
|
||||||
languages: Arc::new(LanguageRegistry::test()),
|
languages: Arc::new(language_registry),
|
||||||
fs: fs.clone(),
|
fs: fs.clone(),
|
||||||
build_window_options: |_, _, _| Default::default(),
|
build_window_options: |_, _, _| Default::default(),
|
||||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||||
@ -536,15 +540,7 @@ impl TestClient {
|
|||||||
root_path: impl AsRef<Path>,
|
root_path: impl AsRef<Path>,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> (ModelHandle<Project>, WorktreeId) {
|
) -> (ModelHandle<Project>, WorktreeId) {
|
||||||
let project = cx.update(|cx| {
|
let project = self.build_empty_local_project(cx);
|
||||||
Project::local(
|
|
||||||
self.client().clone(),
|
|
||||||
self.app_state.user_store.clone(),
|
|
||||||
self.app_state.languages.clone(),
|
|
||||||
self.app_state.fs.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let (worktree, _) = project
|
let (worktree, _) = project
|
||||||
.update(cx, |p, cx| {
|
.update(cx, |p, cx| {
|
||||||
p.find_or_create_local_worktree(root_path, true, cx)
|
p.find_or_create_local_worktree(root_path, true, cx)
|
||||||
@ -557,6 +553,18 @@ impl TestClient {
|
|||||||
(project, worktree.read_with(cx, |tree, _| tree.id()))
|
(project, worktree.read_with(cx, |tree, _| tree.id()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> ModelHandle<Project> {
|
||||||
|
cx.update(|cx| {
|
||||||
|
Project::local(
|
||||||
|
self.client().clone(),
|
||||||
|
self.app_state.user_store.clone(),
|
||||||
|
self.app_state.languages.clone(),
|
||||||
|
self.app_state.fs.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn build_remote_project(
|
pub async fn build_remote_project(
|
||||||
&self,
|
&self,
|
||||||
host_project_id: u64,
|
host_project_id: u64,
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use call::report_call_event_for_channel;
|
use call::report_call_event_for_channel;
|
||||||
use channel::{ChannelBuffer, ChannelBufferEvent, ChannelId};
|
use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId};
|
||||||
use client::proto;
|
use client::{
|
||||||
use clock::ReplicaId;
|
proto::{self, PeerId},
|
||||||
|
Collaborator, ParticipantIndex,
|
||||||
|
};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::Editor;
|
use editor::{CollaborationHub, Editor};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
elements::{ChildView, Label},
|
elements::{ChildView, Label},
|
||||||
@ -13,7 +15,11 @@ use gpui::{
|
|||||||
ViewContext, ViewHandle,
|
ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use std::any::{Any, TypeId};
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use util::ResultExt;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{FollowableItem, Item, ItemHandle},
|
item::{FollowableItem, Item, ItemHandle},
|
||||||
register_followable_item,
|
register_followable_item,
|
||||||
@ -23,7 +29,7 @@ use workspace::{
|
|||||||
|
|
||||||
actions!(channel_view, [Deploy]);
|
actions!(channel_view, [Deploy]);
|
||||||
|
|
||||||
pub(crate) fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
register_followable_item::<ChannelView>(cx)
|
register_followable_item::<ChannelView>(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,9 +42,13 @@ pub struct ChannelView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ChannelView {
|
impl ChannelView {
|
||||||
pub fn deploy(channel_id: ChannelId, workspace: ViewHandle<Workspace>, cx: &mut AppContext) {
|
pub fn open(
|
||||||
|
channel_id: ChannelId,
|
||||||
|
workspace: ViewHandle<Workspace>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Task<Result<ViewHandle<Self>>> {
|
||||||
let pane = workspace.read(cx).active_pane().clone();
|
let pane = workspace.read(cx).active_pane().clone();
|
||||||
let channel_view = Self::open(channel_id, pane.clone(), workspace.clone(), cx);
|
let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let channel_view = channel_view.await?;
|
let channel_view = channel_view.await?;
|
||||||
pane.update(&mut cx, |pane, cx| {
|
pane.update(&mut cx, |pane, cx| {
|
||||||
@ -48,14 +58,13 @@ impl ChannelView {
|
|||||||
&workspace.read(cx).app_state().client,
|
&workspace.read(cx).app_state().client,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
pane.add_item(Box::new(channel_view), true, true, None, cx);
|
pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||||
});
|
});
|
||||||
anyhow::Ok(())
|
anyhow::Ok(channel_view)
|
||||||
})
|
})
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open(
|
pub fn open_in_pane(
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
pane: ViewHandle<Pane>,
|
pane: ViewHandle<Pane>,
|
||||||
workspace: ViewHandle<Workspace>,
|
workspace: ViewHandle<Workspace>,
|
||||||
@ -74,12 +83,13 @@ impl ChannelView {
|
|||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let channel_buffer = channel_buffer.await?;
|
let channel_buffer = channel_buffer.await?;
|
||||||
|
|
||||||
let markdown = markdown.await?;
|
if let Some(markdown) = markdown.await.log_err() {
|
||||||
channel_buffer.update(&mut cx, |buffer, cx| {
|
channel_buffer.update(&mut cx, |buffer, cx| {
|
||||||
buffer.buffer().update(cx, |buffer, cx| {
|
buffer.buffer().update(cx, |buffer, cx| {
|
||||||
buffer.set_language(Some(markdown), cx);
|
buffer.set_language(Some(markdown), cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pane.update(&mut cx, |pane, cx| {
|
pane.update(&mut cx, |pane, cx| {
|
||||||
pane.items_of_type::<Self>()
|
pane.items_of_type::<Self>()
|
||||||
@ -96,40 +106,29 @@ impl ChannelView {
|
|||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let buffer = channel_buffer.read(cx).buffer();
|
let buffer = channel_buffer.read(cx).buffer();
|
||||||
let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx));
|
let editor = cx.add_view(|cx| {
|
||||||
|
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||||
|
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||||
|
channel_buffer.clone(),
|
||||||
|
)));
|
||||||
|
editor
|
||||||
|
});
|
||||||
let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
|
let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
|
||||||
|
|
||||||
cx.subscribe(&project, Self::handle_project_event).detach();
|
|
||||||
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let this = Self {
|
Self {
|
||||||
editor,
|
editor,
|
||||||
project,
|
project,
|
||||||
channel_buffer,
|
channel_buffer,
|
||||||
remote_id: None,
|
remote_id: None,
|
||||||
_editor_event_subscription,
|
_editor_event_subscription,
|
||||||
};
|
}
|
||||||
this.refresh_replica_id_map(cx);
|
|
||||||
this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_project_event(
|
pub fn channel(&self, cx: &AppContext) -> Arc<Channel> {
|
||||||
&mut self,
|
self.channel_buffer.read(cx).channel()
|
||||||
_: ModelHandle<Project>,
|
|
||||||
event: &project::Event,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
match event {
|
|
||||||
project::Event::RemoteIdChanged(_) => {}
|
|
||||||
project::Event::DisconnectedFromHost => {}
|
|
||||||
project::Event::Closed => {}
|
|
||||||
project::Event::CollaboratorUpdated { .. } => {}
|
|
||||||
project::Event::CollaboratorLeft(_) => {}
|
|
||||||
project::Event::CollaboratorJoined(_) => {}
|
|
||||||
_ => return,
|
|
||||||
}
|
|
||||||
self.refresh_replica_id_map(cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_channel_buffer_event(
|
fn handle_channel_buffer_event(
|
||||||
@ -138,50 +137,12 @@ impl ChannelView {
|
|||||||
event: &ChannelBufferEvent,
|
event: &ChannelBufferEvent,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
if let ChannelBufferEvent::Disconnected = event {
|
||||||
ChannelBufferEvent::CollaboratorsChanged => {
|
self.editor.update(cx, |editor, cx| {
|
||||||
self.refresh_replica_id_map(cx);
|
|
||||||
}
|
|
||||||
ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
|
||||||
editor.set_read_only(true);
|
editor.set_read_only(true);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a mapping of channel buffer replica ids to the corresponding
|
|
||||||
/// replica ids in the current project.
|
|
||||||
///
|
|
||||||
/// Using this mapping, a given user can be displayed with the same color
|
|
||||||
/// in the channel buffer as in other files in the project. Users who are
|
|
||||||
/// in the channel buffer but not the project will not have a color.
|
|
||||||
fn refresh_replica_id_map(&self, cx: &mut ViewContext<Self>) {
|
|
||||||
let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default();
|
|
||||||
let project = self.project.read(cx);
|
|
||||||
let channel_buffer = self.channel_buffer.read(cx);
|
|
||||||
project_replica_ids_by_channel_buffer_replica_id
|
|
||||||
.insert(channel_buffer.replica_id(cx), project.replica_id());
|
|
||||||
project_replica_ids_by_channel_buffer_replica_id.extend(
|
|
||||||
channel_buffer
|
|
||||||
.collaborators()
|
|
||||||
.iter()
|
|
||||||
.filter_map(|channel_buffer_collaborator| {
|
|
||||||
project
|
|
||||||
.collaborators()
|
|
||||||
.values()
|
|
||||||
.find_map(|project_collaborator| {
|
|
||||||
(project_collaborator.user_id == channel_buffer_collaborator.user_id)
|
|
||||||
.then_some((
|
|
||||||
channel_buffer_collaborator.replica_id as ReplicaId,
|
|
||||||
project_collaborator.replica_id,
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
}),
|
}
|
||||||
);
|
|
||||||
|
|
||||||
self.editor.update(cx, |editor, cx| {
|
|
||||||
editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,7 +272,7 @@ impl FollowableItem for ChannelView {
|
|||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
|
|
||||||
let open = ChannelView::open(state.channel_id, pane, workspace, cx);
|
let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||||
|
|
||||||
Some(cx.spawn(|mut cx| async move {
|
Some(cx.spawn(|mut cx| async move {
|
||||||
let this = open.await?;
|
let this = open.await?;
|
||||||
@ -371,17 +332,32 @@ impl FollowableItem for ChannelView {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_leader_replica_id(
|
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||||
&mut self,
|
|
||||||
leader_replica_id: Option<u16>,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
editor.set_leader_replica_id(leader_replica_id, cx)
|
editor.set_leader_peer_id(leader_peer_id, cx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
|
fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
|
||||||
Editor::should_unfollow_on_event(event, cx)
|
Editor::should_unfollow_on_event(event, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_project_item(&self, _cx: &AppContext) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
|
||||||
|
|
||||||
|
impl CollaborationHub for ChannelBufferCollaborationHub {
|
||||||
|
fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
|
||||||
|
self.0.read(cx).collaborators()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_participant_indices<'a>(
|
||||||
|
&self,
|
||||||
|
cx: &'a AppContext,
|
||||||
|
) -> &'a HashMap<u64, ParticipantIndex> {
|
||||||
|
self.0.read(cx).user_store().read(cx).participant_indices()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -409,7 +409,7 @@ impl ChatPanel {
|
|||||||
})
|
})
|
||||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||||
if let Some(workspace) = workspace.upgrade(cx) {
|
if let Some(workspace) = workspace.upgrade(cx) {
|
||||||
ChannelView::deploy(channel_id, workspace, cx);
|
ChannelView::open(channel_id, workspace, cx).detach();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.with_tooltip::<OpenChannelNotes>(
|
.with_tooltip::<OpenChannelNotes>(
|
||||||
@ -546,7 +546,7 @@ impl ChatPanel {
|
|||||||
if let Some((chat, _)) = &self.active_chat {
|
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) {
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
ChannelView::deploy(channel_id, workspace, cx);
|
ChannelView::open(channel_id, workspace, cx).detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2755,7 +2755,7 @@ impl CollabPanel {
|
|||||||
|
|
||||||
fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
|
fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
ChannelView::deploy(action.channel_id, workspace, cx);
|
ChannelView::open(action.channel_id, workspace, cx).detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -886,23 +886,20 @@ impl CollabTitlebarItem {
|
|||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> AnyElement<Self> {
|
) -> AnyElement<Self> {
|
||||||
|
let user_id = user.id;
|
||||||
let project_id = workspace.read(cx).project().read(cx).remote_id();
|
let project_id = workspace.read(cx).project().read(cx).remote_id();
|
||||||
let room = ActiveCall::global(cx).read(cx).room();
|
let room = ActiveCall::global(cx).read(cx).room().cloned();
|
||||||
let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
|
let self_peer_id = workspace.read(cx).client().peer_id();
|
||||||
let followed_by_self = room
|
let self_following = workspace.read(cx).is_being_followed(peer_id);
|
||||||
.and_then(|room| {
|
let self_following_initialized = self_following
|
||||||
Some(
|
&& room.as_ref().map_or(false, |room| match project_id {
|
||||||
is_being_followed
|
None => true,
|
||||||
&& room
|
Some(project_id) => room
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.followers_for(peer_id, project_id?)
|
.followers_for(peer_id, project_id)
|
||||||
.iter()
|
.iter()
|
||||||
.any(|&follower| {
|
.any(|&follower| Some(follower) == self_peer_id),
|
||||||
Some(follower) == workspace.read(cx).client().peer_id()
|
});
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
let leader_style = theme.titlebar.leader_avatar;
|
let leader_style = theme.titlebar.leader_avatar;
|
||||||
let follower_style = theme.titlebar.follower_avatar;
|
let follower_style = theme.titlebar.follower_avatar;
|
||||||
@ -921,15 +918,30 @@ impl CollabTitlebarItem {
|
|||||||
.background_color
|
.background_color
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if let Some(replica_id) = replica_id {
|
let participant_index = self
|
||||||
if followed_by_self {
|
.user_store
|
||||||
let selection = theme.editor.replica_selection_style(replica_id).selection;
|
.read(cx)
|
||||||
|
.participant_indices()
|
||||||
|
.get(&user_id)
|
||||||
|
.copied();
|
||||||
|
if let Some(participant_index) = participant_index {
|
||||||
|
if self_following_initialized {
|
||||||
|
let selection = theme
|
||||||
|
.editor
|
||||||
|
.selection_style_for_room_participant(participant_index.0)
|
||||||
|
.selection;
|
||||||
background_color = Color::blend(selection, background_color);
|
background_color = Color::blend(selection, background_color);
|
||||||
background_color.a = 255;
|
background_color.a = 255;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut content = Stack::new()
|
enum TitlebarParticipant {}
|
||||||
|
|
||||||
|
let content = MouseEventHandler::new::<TitlebarParticipant, _>(
|
||||||
|
peer_id.as_u64() as usize,
|
||||||
|
cx,
|
||||||
|
move |_, cx| {
|
||||||
|
Stack::new()
|
||||||
.with_children(user.avatar.as_ref().map(|avatar| {
|
.with_children(user.avatar.as_ref().map(|avatar| {
|
||||||
let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
|
let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
|
||||||
.with_child(Self::render_face(
|
.with_child(Self::render_face(
|
||||||
@ -943,29 +955,14 @@ impl CollabTitlebarItem {
|
|||||||
let project_id = project_id?;
|
let project_id = project_id?;
|
||||||
let room = room?.read(cx);
|
let room = room?.read(cx);
|
||||||
let followers = room.followers_for(peer_id, project_id);
|
let followers = room.followers_for(peer_id, project_id);
|
||||||
|
Some(followers.into_iter().filter_map(|&follower| {
|
||||||
Some(followers.into_iter().flat_map(|&follower| {
|
if Some(follower) == self_peer_id {
|
||||||
let remote_participant =
|
return None;
|
||||||
room.remote_participant_for_peer_id(follower);
|
|
||||||
|
|
||||||
let avatar = remote_participant
|
|
||||||
.and_then(|p| p.user.avatar.clone())
|
|
||||||
.or_else(|| {
|
|
||||||
if follower == workspace.read(cx).client().peer_id()? {
|
|
||||||
workspace
|
|
||||||
.read(cx)
|
|
||||||
.user_store()
|
|
||||||
.read(cx)
|
|
||||||
.current_user()?
|
|
||||||
.avatar
|
|
||||||
.clone()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
})?;
|
let participant =
|
||||||
|
room.remote_participant_for_peer_id(follower)?;
|
||||||
Some(Self::render_face(
|
Some(Self::render_face(
|
||||||
avatar.clone(),
|
participant.user.avatar.clone()?,
|
||||||
follower_style,
|
follower_style,
|
||||||
background_color,
|
background_color,
|
||||||
None,
|
None,
|
||||||
@ -974,15 +971,30 @@ impl CollabTitlebarItem {
|
|||||||
})()
|
})()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten(),
|
.flatten(),
|
||||||
|
)
|
||||||
|
.with_children(
|
||||||
|
self_following_initialized
|
||||||
|
.then(|| self.user_store.read(cx).current_user())
|
||||||
|
.and_then(|user| {
|
||||||
|
Some(Self::render_face(
|
||||||
|
user?.avatar.clone()?,
|
||||||
|
follower_style,
|
||||||
|
background_color,
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut container = face_pile
|
let mut container = face_pile
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(theme.titlebar.leader_selection);
|
.with_style(theme.titlebar.leader_selection);
|
||||||
|
|
||||||
if let Some(replica_id) = replica_id {
|
if let Some(participant_index) = participant_index {
|
||||||
if followed_by_self {
|
if self_following_initialized {
|
||||||
let color = theme.editor.replica_selection_style(replica_id).selection;
|
let color = theme
|
||||||
|
.editor
|
||||||
|
.selection_style_for_room_participant(participant_index.0)
|
||||||
|
.selection;
|
||||||
container = container.with_background_color(color);
|
container = container.with_background_color(color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -990,8 +1002,11 @@ impl CollabTitlebarItem {
|
|||||||
container
|
container
|
||||||
}))
|
}))
|
||||||
.with_children((|| {
|
.with_children((|| {
|
||||||
let replica_id = replica_id?;
|
let participant_index = participant_index?;
|
||||||
let color = theme.editor.replica_selection_style(replica_id).cursor;
|
let color = theme
|
||||||
|
.editor
|
||||||
|
.selection_style_for_room_participant(participant_index.0)
|
||||||
|
.cursor;
|
||||||
Some(
|
Some(
|
||||||
AvatarRibbon::new(color)
|
AvatarRibbon::new(color)
|
||||||
.constrained()
|
.constrained()
|
||||||
@ -1001,17 +1016,35 @@ impl CollabTitlebarItem {
|
|||||||
.bottom(),
|
.bottom(),
|
||||||
)
|
)
|
||||||
})())
|
})())
|
||||||
.into_any();
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(location) = location {
|
match (replica_id, location) {
|
||||||
if let Some(replica_id) = replica_id {
|
// If the user's location isn't known, do nothing.
|
||||||
enum ToggleFollow {}
|
(_, None) => content.into_any(),
|
||||||
|
|
||||||
content = MouseEventHandler::new::<ToggleFollow, _>(
|
// If the user is not in this project, but is in another share project,
|
||||||
replica_id.into(),
|
// join that project.
|
||||||
|
(None, Some(ParticipantLocation::SharedProject { project_id })) => content
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||||
|
let app_state = workspace.read(cx).app_state().clone();
|
||||||
|
workspace::join_remote_project(project_id, user_id, app_state, cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_tooltip::<TitlebarParticipant>(
|
||||||
|
peer_id.as_u64() as usize,
|
||||||
|
format!("Follow {} into external project", user.github_login),
|
||||||
|
Some(Box::new(FollowNextCollaborator)),
|
||||||
|
theme.tooltip.clone(),
|
||||||
cx,
|
cx,
|
||||||
move |_, _| content,
|
|
||||||
)
|
)
|
||||||
|
.into_any(),
|
||||||
|
|
||||||
|
// Otherwise, follow the user in the current window.
|
||||||
|
_ => content
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(MouseButton::Left, move |_, item, cx| {
|
.on_click(MouseButton::Left, move |_, item, cx| {
|
||||||
if let Some(workspace) = item.workspace.upgrade(cx) {
|
if let Some(workspace) = item.workspace.upgrade(cx) {
|
||||||
@ -1022,9 +1055,9 @@ impl CollabTitlebarItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.with_tooltip::<ToggleFollow>(
|
.with_tooltip::<TitlebarParticipant>(
|
||||||
peer_id.as_u64() as usize,
|
peer_id.as_u64() as usize,
|
||||||
if is_being_followed {
|
if self_following {
|
||||||
format!("Unfollow {}", user.github_login)
|
format!("Unfollow {}", user.github_login)
|
||||||
} else {
|
} else {
|
||||||
format!("Follow {}", user.github_login)
|
format!("Follow {}", user.github_login)
|
||||||
@ -1033,35 +1066,8 @@ impl CollabTitlebarItem {
|
|||||||
theme.tooltip.clone(),
|
theme.tooltip.clone(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.into_any();
|
.into_any(),
|
||||||
} else if let ParticipantLocation::SharedProject { project_id } = location {
|
|
||||||
enum JoinProject {}
|
|
||||||
|
|
||||||
let user_id = user.id;
|
|
||||||
content = MouseEventHandler::new::<JoinProject, _>(
|
|
||||||
peer_id.as_u64() as usize,
|
|
||||||
cx,
|
|
||||||
move |_, _| content,
|
|
||||||
)
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
|
||||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
|
||||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
|
||||||
let app_state = workspace.read(cx).app_state().clone();
|
|
||||||
workspace::join_remote_project(project_id, user_id, app_state, cx)
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.with_tooltip::<JoinProject>(
|
|
||||||
peer_id.as_u64() as usize,
|
|
||||||
format!("Follow {} into external project", user.github_login),
|
|
||||||
Some(Box::new(FollowNextCollaborator)),
|
|
||||||
theme.tooltip.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.into_any();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
content
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn location_style(
|
fn location_style(
|
||||||
|
@ -25,7 +25,7 @@ use ::git::diff::DiffHunk;
|
|||||||
use aho_corasick::AhoCorasick;
|
use aho_corasick::AhoCorasick;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use blink_manager::BlinkManager;
|
use blink_manager::BlinkManager;
|
||||||
use client::{ClickhouseEvent, TelemetrySettings};
|
use client::{ClickhouseEvent, Collaborator, ParticipantIndex, TelemetrySettings};
|
||||||
use clock::{Global, ReplicaId};
|
use clock::{Global, ReplicaId};
|
||||||
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
@ -79,6 +79,7 @@ pub use multi_buffer::{
|
|||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
|
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
|
||||||
use rand::{seq::SliceRandom, thread_rng};
|
use rand::{seq::SliceRandom, thread_rng};
|
||||||
|
use rpc::proto::PeerId;
|
||||||
use scroll::{
|
use scroll::{
|
||||||
autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
|
autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
|
||||||
};
|
};
|
||||||
@ -581,11 +582,11 @@ pub struct Editor {
|
|||||||
get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
|
get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
|
||||||
override_text_style: Option<Box<OverrideTextStyle>>,
|
override_text_style: Option<Box<OverrideTextStyle>>,
|
||||||
project: Option<ModelHandle<Project>>,
|
project: Option<ModelHandle<Project>>,
|
||||||
|
collaboration_hub: Option<Box<dyn CollaborationHub>>,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
blink_manager: ModelHandle<BlinkManager>,
|
blink_manager: ModelHandle<BlinkManager>,
|
||||||
pub show_local_selections: bool,
|
pub show_local_selections: bool,
|
||||||
mode: EditorMode,
|
mode: EditorMode,
|
||||||
replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
|
|
||||||
show_gutter: bool,
|
show_gutter: bool,
|
||||||
show_wrap_guides: Option<bool>,
|
show_wrap_guides: Option<bool>,
|
||||||
placeholder_text: Option<Arc<str>>,
|
placeholder_text: Option<Arc<str>>,
|
||||||
@ -609,7 +610,7 @@ pub struct Editor {
|
|||||||
keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
|
keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
|
||||||
input_enabled: bool,
|
input_enabled: bool,
|
||||||
read_only: bool,
|
read_only: bool,
|
||||||
leader_replica_id: Option<u16>,
|
leader_peer_id: Option<PeerId>,
|
||||||
remote_id: Option<ViewId>,
|
remote_id: Option<ViewId>,
|
||||||
hover_state: HoverState,
|
hover_state: HoverState,
|
||||||
gutter_hovered: bool,
|
gutter_hovered: bool,
|
||||||
@ -631,6 +632,15 @@ pub struct EditorSnapshot {
|
|||||||
ongoing_scroll: OngoingScroll,
|
ongoing_scroll: OngoingScroll,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct RemoteSelection {
|
||||||
|
pub replica_id: ReplicaId,
|
||||||
|
pub selection: Selection<Anchor>,
|
||||||
|
pub cursor_shape: CursorShape,
|
||||||
|
pub peer_id: PeerId,
|
||||||
|
pub line_mode: bool,
|
||||||
|
pub participant_index: Option<ParticipantIndex>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct SelectionHistoryEntry {
|
struct SelectionHistoryEntry {
|
||||||
selections: Arc<[Selection<Anchor>]>,
|
selections: Arc<[Selection<Anchor>]>,
|
||||||
@ -1539,12 +1549,12 @@ impl Editor {
|
|||||||
active_diagnostics: None,
|
active_diagnostics: None,
|
||||||
soft_wrap_mode_override,
|
soft_wrap_mode_override,
|
||||||
get_field_editor_theme,
|
get_field_editor_theme,
|
||||||
|
collaboration_hub: project.clone().map(|project| Box::new(project) as _),
|
||||||
project,
|
project,
|
||||||
focused: false,
|
focused: false,
|
||||||
blink_manager: blink_manager.clone(),
|
blink_manager: blink_manager.clone(),
|
||||||
show_local_selections: true,
|
show_local_selections: true,
|
||||||
mode,
|
mode,
|
||||||
replica_id_mapping: None,
|
|
||||||
show_gutter: mode == EditorMode::Full,
|
show_gutter: mode == EditorMode::Full,
|
||||||
show_wrap_guides: None,
|
show_wrap_guides: None,
|
||||||
placeholder_text: None,
|
placeholder_text: None,
|
||||||
@ -1571,7 +1581,7 @@ impl Editor {
|
|||||||
keymap_context_layers: Default::default(),
|
keymap_context_layers: Default::default(),
|
||||||
input_enabled: true,
|
input_enabled: true,
|
||||||
read_only: false,
|
read_only: false,
|
||||||
leader_replica_id: None,
|
leader_peer_id: None,
|
||||||
remote_id: None,
|
remote_id: None,
|
||||||
hover_state: Default::default(),
|
hover_state: Default::default(),
|
||||||
link_go_to_definition_state: Default::default(),
|
link_go_to_definition_state: Default::default(),
|
||||||
@ -1658,8 +1668,8 @@ impl Editor {
|
|||||||
self.buffer.read(cx).replica_id()
|
self.buffer.read(cx).replica_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn leader_replica_id(&self) -> Option<ReplicaId> {
|
pub fn leader_peer_id(&self) -> Option<PeerId> {
|
||||||
self.leader_replica_id
|
self.leader_peer_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn buffer(&self) -> &ModelHandle<MultiBuffer> {
|
pub fn buffer(&self) -> &ModelHandle<MultiBuffer> {
|
||||||
@ -1723,6 +1733,14 @@ impl Editor {
|
|||||||
self.mode
|
self.mode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> {
|
||||||
|
self.collaboration_hub.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_collaboration_hub(&mut self, hub: Box<dyn CollaborationHub>) {
|
||||||
|
self.collaboration_hub = Some(hub);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_placeholder_text(
|
pub fn set_placeholder_text(
|
||||||
&mut self,
|
&mut self,
|
||||||
placeholder_text: impl Into<Arc<str>>,
|
placeholder_text: impl Into<Arc<str>>,
|
||||||
@ -1799,26 +1817,13 @@ impl Editor {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replica_id_map(&self) -> Option<&HashMap<ReplicaId, ReplicaId>> {
|
|
||||||
self.replica_id_mapping.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_replica_id_map(
|
|
||||||
&mut self,
|
|
||||||
mapping: Option<HashMap<ReplicaId, ReplicaId>>,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
self.replica_id_mapping = mapping;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selections_did_change(
|
fn selections_did_change(
|
||||||
&mut self,
|
&mut self,
|
||||||
local: bool,
|
local: bool,
|
||||||
old_cursor_position: &Anchor,
|
old_cursor_position: &Anchor,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
if self.focused && self.leader_replica_id.is_none() {
|
if self.focused && self.leader_peer_id.is_none() {
|
||||||
self.buffer.update(cx, |buffer, cx| {
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.set_active_selections(
|
buffer.set_active_selections(
|
||||||
&self.selections.disjoint_anchors(),
|
&self.selections.disjoint_anchors(),
|
||||||
@ -8625,6 +8630,27 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait CollaborationHub {
|
||||||
|
fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator>;
|
||||||
|
fn user_participant_indices<'a>(
|
||||||
|
&self,
|
||||||
|
cx: &'a AppContext,
|
||||||
|
) -> &'a HashMap<u64, ParticipantIndex>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CollaborationHub for ModelHandle<Project> {
|
||||||
|
fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
|
||||||
|
self.read(cx).collaborators()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_participant_indices<'a>(
|
||||||
|
&self,
|
||||||
|
cx: &'a AppContext,
|
||||||
|
) -> &'a HashMap<u64, ParticipantIndex> {
|
||||||
|
self.read(cx).user_store().read(cx).participant_indices()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn inlay_hint_settings(
|
fn inlay_hint_settings(
|
||||||
location: Anchor,
|
location: Anchor,
|
||||||
snapshot: &MultiBufferSnapshot,
|
snapshot: &MultiBufferSnapshot,
|
||||||
@ -8668,6 +8694,34 @@ fn ending_row(next_selection: &Selection<Point>, display_map: &DisplaySnapshot)
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EditorSnapshot {
|
impl EditorSnapshot {
|
||||||
|
pub fn remote_selections_in_range<'a>(
|
||||||
|
&'a self,
|
||||||
|
range: &'a Range<Anchor>,
|
||||||
|
collaboration_hub: &dyn CollaborationHub,
|
||||||
|
cx: &'a AppContext,
|
||||||
|
) -> impl 'a + Iterator<Item = RemoteSelection> {
|
||||||
|
let participant_indices = collaboration_hub.user_participant_indices(cx);
|
||||||
|
let collaborators_by_peer_id = collaboration_hub.collaborators(cx);
|
||||||
|
let collaborators_by_replica_id = collaborators_by_peer_id
|
||||||
|
.iter()
|
||||||
|
.map(|(_, collaborator)| (collaborator.replica_id, collaborator))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
self.buffer_snapshot
|
||||||
|
.remote_selections_in_range(range)
|
||||||
|
.filter_map(move |(replica_id, line_mode, cursor_shape, selection)| {
|
||||||
|
let collaborator = collaborators_by_replica_id.get(&replica_id)?;
|
||||||
|
let participant_index = participant_indices.get(&collaborator.user_id).copied();
|
||||||
|
Some(RemoteSelection {
|
||||||
|
replica_id,
|
||||||
|
selection,
|
||||||
|
cursor_shape,
|
||||||
|
line_mode,
|
||||||
|
participant_index,
|
||||||
|
peer_id: collaborator.peer_id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
|
pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
|
||||||
self.display_snapshot.buffer_snapshot.language_at(position)
|
self.display_snapshot.buffer_snapshot.language_at(position)
|
||||||
}
|
}
|
||||||
@ -8781,7 +8835,7 @@ impl View for Editor {
|
|||||||
self.focused = true;
|
self.focused = true;
|
||||||
self.buffer.update(cx, |buffer, cx| {
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.finalize_last_transaction(cx);
|
buffer.finalize_last_transaction(cx);
|
||||||
if self.leader_replica_id.is_none() {
|
if self.leader_peer_id.is_none() {
|
||||||
buffer.set_active_selections(
|
buffer.set_active_selections(
|
||||||
&self.selections.disjoint_anchors(),
|
&self.selections.disjoint_anchors(),
|
||||||
self.selections.line_mode,
|
self.selections.line_mode,
|
||||||
|
@ -17,7 +17,6 @@ use crate::{
|
|||||||
},
|
},
|
||||||
mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt,
|
mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt,
|
||||||
};
|
};
|
||||||
use clock::ReplicaId;
|
|
||||||
use collections::{BTreeMap, HashMap};
|
use collections::{BTreeMap, HashMap};
|
||||||
use git::diff::DiffHunkStatus;
|
use git::diff::DiffHunkStatus;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@ -55,6 +54,7 @@ use std::{
|
|||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use text::Point;
|
use text::Point;
|
||||||
|
use theme::SelectionStyle;
|
||||||
use workspace::item::Item;
|
use workspace::item::Item;
|
||||||
|
|
||||||
enum FoldMarkers {}
|
enum FoldMarkers {}
|
||||||
@ -868,14 +868,7 @@ impl EditorElement {
|
|||||||
let corner_radius = 0.15 * layout.position_map.line_height;
|
let corner_radius = 0.15 * layout.position_map.line_height;
|
||||||
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
|
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
|
||||||
|
|
||||||
for (replica_id, selections) in &layout.selections {
|
for (selection_style, selections) in &layout.selections {
|
||||||
let replica_id = *replica_id;
|
|
||||||
let selection_style = if let Some(replica_id) = replica_id {
|
|
||||||
style.replica_selection_style(replica_id)
|
|
||||||
} else {
|
|
||||||
&style.absent_selection
|
|
||||||
};
|
|
||||||
|
|
||||||
for selection in selections {
|
for selection in selections {
|
||||||
self.paint_highlighted_range(
|
self.paint_highlighted_range(
|
||||||
selection.range.clone(),
|
selection.range.clone(),
|
||||||
@ -2193,7 +2186,7 @@ impl Element<Editor> for EditorElement {
|
|||||||
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
|
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)> = Vec::new();
|
let mut selections: Vec<(SelectionStyle, Vec<SelectionLayout>)> = Vec::new();
|
||||||
let mut active_rows = BTreeMap::new();
|
let mut active_rows = BTreeMap::new();
|
||||||
let mut fold_ranges = Vec::new();
|
let mut fold_ranges = Vec::new();
|
||||||
let is_singleton = editor.is_singleton(cx);
|
let is_singleton = editor.is_singleton(cx);
|
||||||
@ -2219,35 +2212,6 @@ impl Element<Editor> for EditorElement {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut remote_selections = HashMap::default();
|
|
||||||
for (replica_id, line_mode, cursor_shape, selection) in snapshot
|
|
||||||
.buffer_snapshot
|
|
||||||
.remote_selections_in_range(&(start_anchor..end_anchor))
|
|
||||||
{
|
|
||||||
let replica_id = if let Some(mapping) = &editor.replica_id_mapping {
|
|
||||||
mapping.get(&replica_id).copied()
|
|
||||||
} else {
|
|
||||||
Some(replica_id)
|
|
||||||
};
|
|
||||||
|
|
||||||
// The local selections match the leader's selections.
|
|
||||||
if replica_id.is_some() && replica_id == editor.leader_replica_id {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
remote_selections
|
|
||||||
.entry(replica_id)
|
|
||||||
.or_insert(Vec::new())
|
|
||||||
.push(SelectionLayout::new(
|
|
||||||
selection,
|
|
||||||
line_mode,
|
|
||||||
cursor_shape,
|
|
||||||
&snapshot.display_snapshot,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
selections.extend(remote_selections);
|
|
||||||
|
|
||||||
let mut newest_selection_head = None;
|
let mut newest_selection_head = None;
|
||||||
|
|
||||||
if editor.show_local_selections {
|
if editor.show_local_selections {
|
||||||
@ -2282,19 +2246,58 @@ impl Element<Editor> for EditorElement {
|
|||||||
layouts.push(layout);
|
layouts.push(layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the local selections in the leader's color when following.
|
selections.push((style.selection, layouts));
|
||||||
let local_replica_id = if let Some(leader_replica_id) = editor.leader_replica_id {
|
|
||||||
leader_replica_id
|
|
||||||
} else {
|
|
||||||
let replica_id = editor.replica_id(cx);
|
|
||||||
if let Some(mapping) = &editor.replica_id_mapping {
|
|
||||||
mapping.get(&replica_id).copied().unwrap_or(replica_id)
|
|
||||||
} else {
|
|
||||||
replica_id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(collaboration_hub) = &editor.collaboration_hub {
|
||||||
|
// When following someone, render the local selections in their color.
|
||||||
|
if let Some(leader_id) = editor.leader_peer_id {
|
||||||
|
if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
|
||||||
|
if let Some(participant_index) = collaboration_hub
|
||||||
|
.user_participant_indices(cx)
|
||||||
|
.get(&collaborator.user_id)
|
||||||
|
{
|
||||||
|
if let Some((local_selection_style, _)) = selections.first_mut() {
|
||||||
|
*local_selection_style =
|
||||||
|
style.selection_style_for_room_participant(participant_index.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut remote_selections = HashMap::default();
|
||||||
|
for selection in snapshot.remote_selections_in_range(
|
||||||
|
&(start_anchor..end_anchor),
|
||||||
|
collaboration_hub.as_ref(),
|
||||||
|
cx,
|
||||||
|
) {
|
||||||
|
let selection_style = if let Some(participant_index) = selection.participant_index {
|
||||||
|
style.selection_style_for_room_participant(participant_index.0)
|
||||||
|
} else {
|
||||||
|
style.absent_selection
|
||||||
};
|
};
|
||||||
|
|
||||||
selections.push((Some(local_replica_id), layouts));
|
// Don't re-render the leader's selections, since the local selections
|
||||||
|
// match theirs.
|
||||||
|
if Some(selection.peer_id) == editor.leader_peer_id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
remote_selections
|
||||||
|
.entry(selection.replica_id)
|
||||||
|
.or_insert((selection_style, Vec::new()))
|
||||||
|
.1
|
||||||
|
.push(SelectionLayout::new(
|
||||||
|
selection.selection,
|
||||||
|
selection.line_mode,
|
||||||
|
selection.cursor_shape,
|
||||||
|
&snapshot.display_snapshot,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
selections.extend(remote_selections.into_values());
|
||||||
}
|
}
|
||||||
|
|
||||||
let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
|
let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
|
||||||
@ -2686,7 +2689,7 @@ pub struct LayoutState {
|
|||||||
blocks: Vec<BlockLayout>,
|
blocks: Vec<BlockLayout>,
|
||||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||||
fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
|
fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
|
||||||
selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)>,
|
selections: Vec<(SelectionStyle, Vec<SelectionLayout>)>,
|
||||||
scrollbar_row_range: Range<f32>,
|
scrollbar_row_range: Range<f32>,
|
||||||
show_scrollbars: bool,
|
show_scrollbars: bool,
|
||||||
is_singleton: bool,
|
is_singleton: bool,
|
||||||
|
@ -17,7 +17,7 @@ use language::{
|
|||||||
SelectionGoal,
|
SelectionGoal,
|
||||||
};
|
};
|
||||||
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
|
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
|
||||||
use rpc::proto::{self, update_view};
|
use rpc::proto::{self, update_view, PeerId};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
@ -156,13 +156,9 @@ impl FollowableItem for Editor {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_leader_replica_id(
|
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||||
&mut self,
|
self.leader_peer_id = leader_peer_id;
|
||||||
leader_replica_id: Option<u16>,
|
if self.leader_peer_id.is_some() {
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
self.leader_replica_id = leader_replica_id;
|
|
||||||
if self.leader_replica_id.is_some() {
|
|
||||||
self.buffer.update(cx, |buffer, cx| {
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.remove_active_selections(cx);
|
buffer.remove_active_selections(cx);
|
||||||
});
|
});
|
||||||
@ -309,6 +305,10 @@ impl FollowableItem for Editor {
|
|||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_project_item(&self, _cx: &AppContext) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_editor_from_message(
|
async fn update_editor_from_message(
|
||||||
|
@ -11,7 +11,7 @@ mod project_tests;
|
|||||||
mod worktree_tests;
|
mod worktree_tests;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use client::{proto, Client, TypedEnvelope, UserId, UserStore};
|
use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||||
use copilot::Copilot;
|
use copilot::Copilot;
|
||||||
@ -253,13 +253,6 @@ enum ProjectClientState {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Collaborator {
|
|
||||||
pub peer_id: proto::PeerId,
|
|
||||||
pub replica_id: ReplicaId,
|
|
||||||
pub user_id: UserId,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
LanguageServerAdded(LanguageServerId),
|
LanguageServerAdded(LanguageServerId),
|
||||||
@ -8252,16 +8245,6 @@ impl Entity for Project {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collaborator {
|
|
||||||
fn from_proto(message: proto::Collaborator) -> Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
|
|
||||||
replica_id: message.replica_id as ReplicaId,
|
|
||||||
user_id: message.user_id as UserId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
|
impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
|
||||||
fn from((worktree_id, path): (WorktreeId, P)) -> Self {
|
fn from((worktree_id, path): (WorktreeId, P)) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -23,154 +23,152 @@ message Envelope {
|
|||||||
CreateRoomResponse create_room_response = 10;
|
CreateRoomResponse create_room_response = 10;
|
||||||
JoinRoom join_room = 11;
|
JoinRoom join_room = 11;
|
||||||
JoinRoomResponse join_room_response = 12;
|
JoinRoomResponse join_room_response = 12;
|
||||||
RejoinRoom rejoin_room = 108;
|
RejoinRoom rejoin_room = 13;
|
||||||
RejoinRoomResponse rejoin_room_response = 109;
|
RejoinRoomResponse rejoin_room_response = 14;
|
||||||
LeaveRoom leave_room = 13;
|
LeaveRoom leave_room = 15;
|
||||||
Call call = 14;
|
Call call = 16;
|
||||||
IncomingCall incoming_call = 15;
|
IncomingCall incoming_call = 17;
|
||||||
CallCanceled call_canceled = 16;
|
CallCanceled call_canceled = 18;
|
||||||
CancelCall cancel_call = 17;
|
CancelCall cancel_call = 19;
|
||||||
DeclineCall decline_call = 18;
|
DeclineCall decline_call = 20;
|
||||||
UpdateParticipantLocation update_participant_location = 19;
|
UpdateParticipantLocation update_participant_location = 21;
|
||||||
RoomUpdated room_updated = 20;
|
RoomUpdated room_updated = 22;
|
||||||
|
|
||||||
ShareProject share_project = 21;
|
ShareProject share_project = 23;
|
||||||
ShareProjectResponse share_project_response = 22;
|
ShareProjectResponse share_project_response = 24;
|
||||||
UnshareProject unshare_project = 23;
|
UnshareProject unshare_project = 25;
|
||||||
JoinProject join_project = 24;
|
JoinProject join_project = 26;
|
||||||
JoinProjectResponse join_project_response = 25;
|
JoinProjectResponse join_project_response = 27;
|
||||||
LeaveProject leave_project = 26;
|
LeaveProject leave_project = 28;
|
||||||
AddProjectCollaborator add_project_collaborator = 27;
|
AddProjectCollaborator add_project_collaborator = 29;
|
||||||
UpdateProjectCollaborator update_project_collaborator = 110;
|
UpdateProjectCollaborator update_project_collaborator = 30;
|
||||||
RemoveProjectCollaborator remove_project_collaborator = 28;
|
RemoveProjectCollaborator remove_project_collaborator = 31;
|
||||||
|
|
||||||
GetDefinition get_definition = 29;
|
GetDefinition get_definition = 32;
|
||||||
GetDefinitionResponse get_definition_response = 30;
|
GetDefinitionResponse get_definition_response = 33;
|
||||||
GetTypeDefinition get_type_definition = 31;
|
GetTypeDefinition get_type_definition = 34;
|
||||||
GetTypeDefinitionResponse get_type_definition_response = 32;
|
GetTypeDefinitionResponse get_type_definition_response = 35;
|
||||||
GetReferences get_references = 33;
|
GetReferences get_references = 36;
|
||||||
GetReferencesResponse get_references_response = 34;
|
GetReferencesResponse get_references_response = 37;
|
||||||
GetDocumentHighlights get_document_highlights = 35;
|
GetDocumentHighlights get_document_highlights = 38;
|
||||||
GetDocumentHighlightsResponse get_document_highlights_response = 36;
|
GetDocumentHighlightsResponse get_document_highlights_response = 39;
|
||||||
GetProjectSymbols get_project_symbols = 37;
|
GetProjectSymbols get_project_symbols = 40;
|
||||||
GetProjectSymbolsResponse get_project_symbols_response = 38;
|
GetProjectSymbolsResponse get_project_symbols_response = 41;
|
||||||
OpenBufferForSymbol open_buffer_for_symbol = 39;
|
OpenBufferForSymbol open_buffer_for_symbol = 42;
|
||||||
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 40;
|
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 43;
|
||||||
|
|
||||||
UpdateProject update_project = 41;
|
UpdateProject update_project = 44;
|
||||||
UpdateWorktree update_worktree = 43;
|
UpdateWorktree update_worktree = 45;
|
||||||
|
|
||||||
CreateProjectEntry create_project_entry = 45;
|
CreateProjectEntry create_project_entry = 46;
|
||||||
RenameProjectEntry rename_project_entry = 46;
|
RenameProjectEntry rename_project_entry = 47;
|
||||||
CopyProjectEntry copy_project_entry = 47;
|
CopyProjectEntry copy_project_entry = 48;
|
||||||
DeleteProjectEntry delete_project_entry = 48;
|
DeleteProjectEntry delete_project_entry = 49;
|
||||||
ProjectEntryResponse project_entry_response = 49;
|
ProjectEntryResponse project_entry_response = 50;
|
||||||
ExpandProjectEntry expand_project_entry = 114;
|
ExpandProjectEntry expand_project_entry = 51;
|
||||||
ExpandProjectEntryResponse expand_project_entry_response = 115;
|
ExpandProjectEntryResponse expand_project_entry_response = 52;
|
||||||
|
|
||||||
UpdateDiagnosticSummary update_diagnostic_summary = 50;
|
UpdateDiagnosticSummary update_diagnostic_summary = 53;
|
||||||
StartLanguageServer start_language_server = 51;
|
StartLanguageServer start_language_server = 54;
|
||||||
UpdateLanguageServer update_language_server = 52;
|
UpdateLanguageServer update_language_server = 55;
|
||||||
|
|
||||||
OpenBufferById open_buffer_by_id = 53;
|
OpenBufferById open_buffer_by_id = 56;
|
||||||
OpenBufferByPath open_buffer_by_path = 54;
|
OpenBufferByPath open_buffer_by_path = 57;
|
||||||
OpenBufferResponse open_buffer_response = 55;
|
OpenBufferResponse open_buffer_response = 58;
|
||||||
CreateBufferForPeer create_buffer_for_peer = 56;
|
CreateBufferForPeer create_buffer_for_peer = 59;
|
||||||
UpdateBuffer update_buffer = 57;
|
UpdateBuffer update_buffer = 60;
|
||||||
UpdateBufferFile update_buffer_file = 58;
|
UpdateBufferFile update_buffer_file = 61;
|
||||||
SaveBuffer save_buffer = 59;
|
SaveBuffer save_buffer = 62;
|
||||||
BufferSaved buffer_saved = 60;
|
BufferSaved buffer_saved = 63;
|
||||||
BufferReloaded buffer_reloaded = 61;
|
BufferReloaded buffer_reloaded = 64;
|
||||||
ReloadBuffers reload_buffers = 62;
|
ReloadBuffers reload_buffers = 65;
|
||||||
ReloadBuffersResponse reload_buffers_response = 63;
|
ReloadBuffersResponse reload_buffers_response = 66;
|
||||||
SynchronizeBuffers synchronize_buffers = 200;
|
SynchronizeBuffers synchronize_buffers = 67;
|
||||||
SynchronizeBuffersResponse synchronize_buffers_response = 201;
|
SynchronizeBuffersResponse synchronize_buffers_response = 68;
|
||||||
FormatBuffers format_buffers = 64;
|
FormatBuffers format_buffers = 69;
|
||||||
FormatBuffersResponse format_buffers_response = 65;
|
FormatBuffersResponse format_buffers_response = 70;
|
||||||
GetCompletions get_completions = 66;
|
GetCompletions get_completions = 71;
|
||||||
GetCompletionsResponse get_completions_response = 67;
|
GetCompletionsResponse get_completions_response = 72;
|
||||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 68;
|
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73;
|
||||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 69;
|
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74;
|
||||||
GetCodeActions get_code_actions = 70;
|
GetCodeActions get_code_actions = 75;
|
||||||
GetCodeActionsResponse get_code_actions_response = 71;
|
GetCodeActionsResponse get_code_actions_response = 76;
|
||||||
GetHover get_hover = 72;
|
GetHover get_hover = 77;
|
||||||
GetHoverResponse get_hover_response = 73;
|
GetHoverResponse get_hover_response = 78;
|
||||||
ApplyCodeAction apply_code_action = 74;
|
ApplyCodeAction apply_code_action = 79;
|
||||||
ApplyCodeActionResponse apply_code_action_response = 75;
|
ApplyCodeActionResponse apply_code_action_response = 80;
|
||||||
PrepareRename prepare_rename = 76;
|
PrepareRename prepare_rename = 81;
|
||||||
PrepareRenameResponse prepare_rename_response = 77;
|
PrepareRenameResponse prepare_rename_response = 82;
|
||||||
PerformRename perform_rename = 78;
|
PerformRename perform_rename = 83;
|
||||||
PerformRenameResponse perform_rename_response = 79;
|
PerformRenameResponse perform_rename_response = 84;
|
||||||
SearchProject search_project = 80;
|
SearchProject search_project = 85;
|
||||||
SearchProjectResponse search_project_response = 81;
|
SearchProjectResponse search_project_response = 86;
|
||||||
|
|
||||||
UpdateContacts update_contacts = 92;
|
UpdateContacts update_contacts = 87;
|
||||||
UpdateInviteInfo update_invite_info = 93;
|
UpdateInviteInfo update_invite_info = 88;
|
||||||
ShowContacts show_contacts = 94;
|
ShowContacts show_contacts = 89;
|
||||||
|
|
||||||
GetUsers get_users = 95;
|
GetUsers get_users = 90;
|
||||||
FuzzySearchUsers fuzzy_search_users = 96;
|
FuzzySearchUsers fuzzy_search_users = 91;
|
||||||
UsersResponse users_response = 97;
|
UsersResponse users_response = 92;
|
||||||
RequestContact request_contact = 98;
|
RequestContact request_contact = 93;
|
||||||
RespondToContactRequest respond_to_contact_request = 99;
|
RespondToContactRequest respond_to_contact_request = 94;
|
||||||
RemoveContact remove_contact = 100;
|
RemoveContact remove_contact = 95;
|
||||||
|
|
||||||
Follow follow = 101;
|
Follow follow = 96;
|
||||||
FollowResponse follow_response = 102;
|
FollowResponse follow_response = 97;
|
||||||
UpdateFollowers update_followers = 103;
|
UpdateFollowers update_followers = 98;
|
||||||
Unfollow unfollow = 104;
|
Unfollow unfollow = 99;
|
||||||
GetPrivateUserInfo get_private_user_info = 105;
|
GetPrivateUserInfo get_private_user_info = 100;
|
||||||
GetPrivateUserInfoResponse get_private_user_info_response = 106;
|
GetPrivateUserInfoResponse get_private_user_info_response = 101;
|
||||||
UpdateDiffBase update_diff_base = 107;
|
UpdateDiffBase update_diff_base = 102;
|
||||||
|
|
||||||
OnTypeFormatting on_type_formatting = 111;
|
OnTypeFormatting on_type_formatting = 103;
|
||||||
OnTypeFormattingResponse on_type_formatting_response = 112;
|
OnTypeFormattingResponse on_type_formatting_response = 104;
|
||||||
|
|
||||||
UpdateWorktreeSettings update_worktree_settings = 113;
|
UpdateWorktreeSettings update_worktree_settings = 105;
|
||||||
|
|
||||||
InlayHints inlay_hints = 116;
|
InlayHints inlay_hints = 106;
|
||||||
InlayHintsResponse inlay_hints_response = 117;
|
InlayHintsResponse inlay_hints_response = 107;
|
||||||
ResolveInlayHint resolve_inlay_hint = 137;
|
ResolveInlayHint resolve_inlay_hint = 108;
|
||||||
ResolveInlayHintResponse resolve_inlay_hint_response = 138;
|
ResolveInlayHintResponse resolve_inlay_hint_response = 109;
|
||||||
RefreshInlayHints refresh_inlay_hints = 118;
|
RefreshInlayHints refresh_inlay_hints = 110;
|
||||||
|
|
||||||
CreateChannel create_channel = 119;
|
CreateChannel create_channel = 111;
|
||||||
CreateChannelResponse create_channel_response = 120;
|
CreateChannelResponse create_channel_response = 112;
|
||||||
InviteChannelMember invite_channel_member = 121;
|
InviteChannelMember invite_channel_member = 113;
|
||||||
RemoveChannelMember remove_channel_member = 122;
|
RemoveChannelMember remove_channel_member = 114;
|
||||||
RespondToChannelInvite respond_to_channel_invite = 123;
|
RespondToChannelInvite respond_to_channel_invite = 115;
|
||||||
UpdateChannels update_channels = 124;
|
UpdateChannels update_channels = 116;
|
||||||
JoinChannel join_channel = 125;
|
JoinChannel join_channel = 117;
|
||||||
DeleteChannel delete_channel = 126;
|
DeleteChannel delete_channel = 118;
|
||||||
GetChannelMembers get_channel_members = 127;
|
GetChannelMembers get_channel_members = 119;
|
||||||
GetChannelMembersResponse get_channel_members_response = 128;
|
GetChannelMembersResponse get_channel_members_response = 120;
|
||||||
SetChannelMemberAdmin set_channel_member_admin = 129;
|
SetChannelMemberAdmin set_channel_member_admin = 121;
|
||||||
RenameChannel rename_channel = 130;
|
RenameChannel rename_channel = 122;
|
||||||
RenameChannelResponse rename_channel_response = 154;
|
RenameChannelResponse rename_channel_response = 123;
|
||||||
|
|
||||||
JoinChannelBuffer join_channel_buffer = 131;
|
JoinChannelBuffer join_channel_buffer = 124;
|
||||||
JoinChannelBufferResponse join_channel_buffer_response = 132;
|
JoinChannelBufferResponse join_channel_buffer_response = 125;
|
||||||
UpdateChannelBuffer update_channel_buffer = 133;
|
UpdateChannelBuffer update_channel_buffer = 126;
|
||||||
LeaveChannelBuffer leave_channel_buffer = 134;
|
LeaveChannelBuffer leave_channel_buffer = 127;
|
||||||
AddChannelBufferCollaborator add_channel_buffer_collaborator = 135;
|
UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128;
|
||||||
RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136;
|
RejoinChannelBuffers rejoin_channel_buffers = 129;
|
||||||
UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139;
|
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130;
|
||||||
RejoinChannelBuffers rejoin_channel_buffers = 140;
|
|
||||||
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141;
|
|
||||||
|
|
||||||
JoinChannelChat join_channel_chat = 142;
|
JoinChannelChat join_channel_chat = 131;
|
||||||
JoinChannelChatResponse join_channel_chat_response = 143;
|
JoinChannelChatResponse join_channel_chat_response = 132;
|
||||||
LeaveChannelChat leave_channel_chat = 144;
|
LeaveChannelChat leave_channel_chat = 133;
|
||||||
SendChannelMessage send_channel_message = 145;
|
SendChannelMessage send_channel_message = 134;
|
||||||
SendChannelMessageResponse send_channel_message_response = 146;
|
SendChannelMessageResponse send_channel_message_response = 135;
|
||||||
ChannelMessageSent channel_message_sent = 147;
|
ChannelMessageSent channel_message_sent = 136;
|
||||||
GetChannelMessages get_channel_messages = 148;
|
GetChannelMessages get_channel_messages = 137;
|
||||||
GetChannelMessagesResponse get_channel_messages_response = 149;
|
GetChannelMessagesResponse get_channel_messages_response = 138;
|
||||||
RemoveChannelMessage remove_channel_message = 150;
|
RemoveChannelMessage remove_channel_message = 139;
|
||||||
|
|
||||||
LinkChannel link_channel = 151;
|
LinkChannel link_channel = 140;
|
||||||
UnlinkChannel unlink_channel = 152;
|
UnlinkChannel unlink_channel = 141;
|
||||||
MoveChannel move_channel = 153; // Current max: 154
|
MoveChannel move_channel = 142;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,6 +256,7 @@ message Participant {
|
|||||||
PeerId peer_id = 2;
|
PeerId peer_id = 2;
|
||||||
repeated ParticipantProject projects = 3;
|
repeated ParticipantProject projects = 3;
|
||||||
ParticipantLocation location = 4;
|
ParticipantLocation location = 4;
|
||||||
|
uint32 participant_index = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message PendingParticipant {
|
message PendingParticipant {
|
||||||
@ -440,20 +439,9 @@ message RemoveProjectCollaborator {
|
|||||||
PeerId peer_id = 2;
|
PeerId peer_id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AddChannelBufferCollaborator {
|
message UpdateChannelBufferCollaborators {
|
||||||
uint64 channel_id = 1;
|
uint64 channel_id = 1;
|
||||||
Collaborator collaborator = 2;
|
repeated Collaborator collaborators = 2;
|
||||||
}
|
|
||||||
|
|
||||||
message RemoveChannelBufferCollaborator {
|
|
||||||
uint64 channel_id = 1;
|
|
||||||
PeerId peer_id = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message UpdateChannelBufferCollaborator {
|
|
||||||
uint64 channel_id = 1;
|
|
||||||
PeerId old_peer_id = 2;
|
|
||||||
PeerId new_peer_id = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetDefinition {
|
message GetDefinition {
|
||||||
@ -1213,8 +1201,9 @@ message UpdateDiagnostics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message Follow {
|
message Follow {
|
||||||
uint64 project_id = 1;
|
uint64 room_id = 1;
|
||||||
PeerId leader_id = 2;
|
optional uint64 project_id = 2;
|
||||||
|
PeerId leader_id = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message FollowResponse {
|
message FollowResponse {
|
||||||
@ -1223,18 +1212,20 @@ message FollowResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message UpdateFollowers {
|
message UpdateFollowers {
|
||||||
uint64 project_id = 1;
|
uint64 room_id = 1;
|
||||||
repeated PeerId follower_ids = 2;
|
optional uint64 project_id = 2;
|
||||||
|
repeated PeerId follower_ids = 3;
|
||||||
oneof variant {
|
oneof variant {
|
||||||
UpdateActiveView update_active_view = 3;
|
UpdateActiveView update_active_view = 4;
|
||||||
View create_view = 4;
|
View create_view = 5;
|
||||||
UpdateView update_view = 5;
|
UpdateView update_view = 6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message Unfollow {
|
message Unfollow {
|
||||||
uint64 project_id = 1;
|
uint64 room_id = 1;
|
||||||
PeerId leader_id = 2;
|
optional uint64 project_id = 2;
|
||||||
|
PeerId leader_id = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetPrivateUserInfo {}
|
message GetPrivateUserInfo {}
|
||||||
|
@ -270,9 +270,7 @@ messages!(
|
|||||||
(JoinChannelBufferResponse, Foreground),
|
(JoinChannelBufferResponse, Foreground),
|
||||||
(LeaveChannelBuffer, Background),
|
(LeaveChannelBuffer, Background),
|
||||||
(UpdateChannelBuffer, Foreground),
|
(UpdateChannelBuffer, Foreground),
|
||||||
(RemoveChannelBufferCollaborator, Foreground),
|
(UpdateChannelBufferCollaborators, Foreground),
|
||||||
(AddChannelBufferCollaborator, Foreground),
|
|
||||||
(UpdateChannelBufferCollaborator, Foreground),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
request_messages!(
|
request_messages!(
|
||||||
@ -364,7 +362,6 @@ entity_messages!(
|
|||||||
CreateProjectEntry,
|
CreateProjectEntry,
|
||||||
DeleteProjectEntry,
|
DeleteProjectEntry,
|
||||||
ExpandProjectEntry,
|
ExpandProjectEntry,
|
||||||
Follow,
|
|
||||||
FormatBuffers,
|
FormatBuffers,
|
||||||
GetCodeActions,
|
GetCodeActions,
|
||||||
GetCompletions,
|
GetCompletions,
|
||||||
@ -392,12 +389,10 @@ entity_messages!(
|
|||||||
SearchProject,
|
SearchProject,
|
||||||
StartLanguageServer,
|
StartLanguageServer,
|
||||||
SynchronizeBuffers,
|
SynchronizeBuffers,
|
||||||
Unfollow,
|
|
||||||
UnshareProject,
|
UnshareProject,
|
||||||
UpdateBuffer,
|
UpdateBuffer,
|
||||||
UpdateBufferFile,
|
UpdateBufferFile,
|
||||||
UpdateDiagnosticSummary,
|
UpdateDiagnosticSummary,
|
||||||
UpdateFollowers,
|
|
||||||
UpdateLanguageServer,
|
UpdateLanguageServer,
|
||||||
UpdateProject,
|
UpdateProject,
|
||||||
UpdateProjectCollaborator,
|
UpdateProjectCollaborator,
|
||||||
@ -410,10 +405,8 @@ entity_messages!(
|
|||||||
channel_id,
|
channel_id,
|
||||||
ChannelMessageSent,
|
ChannelMessageSent,
|
||||||
UpdateChannelBuffer,
|
UpdateChannelBuffer,
|
||||||
RemoveChannelBufferCollaborator,
|
|
||||||
RemoveChannelMessage,
|
RemoveChannelMessage,
|
||||||
AddChannelBufferCollaborator,
|
UpdateChannelBufferCollaborators
|
||||||
UpdateChannelBufferCollaborator
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const KIB: usize = 1024;
|
const KIB: usize = 1024;
|
||||||
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
|||||||
pub use peer::*;
|
pub use peer::*;
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
pub const PROTOCOL_VERSION: u32 = 63;
|
pub const PROTOCOL_VERSION: u32 = 64;
|
||||||
|
@ -1065,13 +1065,12 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Editor {
|
impl Editor {
|
||||||
pub fn replica_selection_style(&self, replica_id: u16) -> &SelectionStyle {
|
pub fn selection_style_for_room_participant(&self, participant_index: u32) -> SelectionStyle {
|
||||||
let style_ix = replica_id as usize % (self.guest_selections.len() + 1);
|
if self.guest_selections.is_empty() {
|
||||||
if style_ix == 0 {
|
return SelectionStyle::default();
|
||||||
&self.selection
|
|
||||||
} else {
|
|
||||||
&self.guest_selections[style_ix - 1]
|
|
||||||
}
|
}
|
||||||
|
let style_ix = participant_index as usize % self.guest_selections.len();
|
||||||
|
self.guest_selections[style_ix]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,7 +171,7 @@ impl Vim {
|
|||||||
self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
|
self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
|
||||||
Event::SelectionsChanged { local: true } => {
|
Event::SelectionsChanged { local: true } => {
|
||||||
let editor = editor.read(cx);
|
let editor = editor.read(cx);
|
||||||
if editor.leader_replica_id().is_none() {
|
if editor.leader_peer_id().is_none() {
|
||||||
let newest = editor.selections.newest::<usize>(cx);
|
let newest = editor.selections.newest::<usize>(cx);
|
||||||
local_selections_changed(newest, cx);
|
local_selections_changed(newest, cx);
|
||||||
}
|
}
|
||||||
@ -195,9 +195,8 @@ impl Vim {
|
|||||||
if editor_mode == EditorMode::Full
|
if editor_mode == EditorMode::Full
|
||||||
&& !newest_selection_empty
|
&& !newest_selection_empty
|
||||||
&& self.state().mode == Mode::Normal
|
&& self.state().mode == Mode::Normal
|
||||||
// if leader_replica_id is set, then you're following someone else's cursor
|
// When following someone, don't switch vim mode.
|
||||||
// don't switch vim mode.
|
&& editor.leader_peer_id().is_none()
|
||||||
&& editor.leader_replica_id().is_none()
|
|
||||||
{
|
{
|
||||||
self.switch_mode(Mode::Visual, true, cx);
|
self.switch_mode(Mode::Visual, true, cx);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,10 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
|
use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::{proto, Client};
|
use client::{
|
||||||
|
proto::{self, PeerId},
|
||||||
|
Client,
|
||||||
|
};
|
||||||
use gpui::geometry::vector::Vector2F;
|
use gpui::geometry::vector::Vector2F;
|
||||||
use gpui::AnyWindowHandle;
|
use gpui::AnyWindowHandle;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@ -401,6 +404,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
|||||||
if let Some(followed_item) = self.to_followable_item_handle(cx) {
|
if let Some(followed_item) = self.to_followable_item_handle(cx) {
|
||||||
if let Some(message) = followed_item.to_state_proto(cx) {
|
if let Some(message) = followed_item.to_state_proto(cx) {
|
||||||
workspace.update_followers(
|
workspace.update_followers(
|
||||||
|
followed_item.is_project_item(cx),
|
||||||
proto::update_followers::Variant::CreateView(proto::View {
|
proto::update_followers::Variant::CreateView(proto::View {
|
||||||
id: followed_item
|
id: followed_item
|
||||||
.remote_id(&workspace.app_state.client, cx)
|
.remote_id(&workspace.app_state.client, cx)
|
||||||
@ -436,6 +440,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(item) = item.to_followable_item_handle(cx) {
|
if let Some(item) = item.to_followable_item_handle(cx) {
|
||||||
|
let is_project_item = item.is_project_item(cx);
|
||||||
let leader_id = workspace.leader_for_pane(&pane);
|
let leader_id = workspace.leader_for_pane(&pane);
|
||||||
|
|
||||||
if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
|
if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
|
||||||
@ -455,6 +460,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
|||||||
move |this, cx| {
|
move |this, cx| {
|
||||||
pending_update_scheduled.store(false, Ordering::SeqCst);
|
pending_update_scheduled.store(false, Ordering::SeqCst);
|
||||||
this.update_followers(
|
this.update_followers(
|
||||||
|
is_project_item,
|
||||||
proto::update_followers::Variant::UpdateView(
|
proto::update_followers::Variant::UpdateView(
|
||||||
proto::UpdateView {
|
proto::UpdateView {
|
||||||
id: item
|
id: item
|
||||||
@ -692,14 +698,15 @@ pub trait FollowableItem: Item {
|
|||||||
message: proto::update_view::Variant,
|
message: proto::update_view::Variant,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>>;
|
) -> Task<Result<()>>;
|
||||||
|
fn is_project_item(&self, cx: &AppContext) -> bool;
|
||||||
|
|
||||||
fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
|
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
|
||||||
fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
|
fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FollowableItemHandle: ItemHandle {
|
pub trait FollowableItemHandle: ItemHandle {
|
||||||
fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
|
fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
|
||||||
fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut WindowContext);
|
fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
|
||||||
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
|
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
|
||||||
fn add_event_to_update_proto(
|
fn add_event_to_update_proto(
|
||||||
&self,
|
&self,
|
||||||
@ -714,6 +721,7 @@ pub trait FollowableItemHandle: ItemHandle {
|
|||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<()>>;
|
) -> Task<Result<()>>;
|
||||||
fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
|
fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
|
||||||
|
fn is_project_item(&self, cx: &AppContext) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
|
impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
|
||||||
@ -726,10 +734,8 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut WindowContext) {
|
fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext) {
|
||||||
self.update(cx, |this, cx| {
|
self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx))
|
||||||
this.set_leader_replica_id(leader_replica_id, cx)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||||
@ -765,6 +771,10 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_project_item(&self, cx: &AppContext) -> bool {
|
||||||
|
self.read(cx).is_project_item(cx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
@ -190,25 +190,23 @@ impl Member {
|
|||||||
})
|
})
|
||||||
.and_then(|leader_id| {
|
.and_then(|leader_id| {
|
||||||
let room = active_call?.read(cx).room()?.read(cx);
|
let room = active_call?.read(cx).room()?.read(cx);
|
||||||
let collaborator = project.read(cx).collaborators().get(leader_id)?;
|
room.remote_participant_for_peer_id(*leader_id)
|
||||||
let participant = room.remote_participant_for_peer_id(*leader_id)?;
|
|
||||||
Some((collaborator.replica_id, participant))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let border = if let Some((replica_id, _)) = leader.as_ref() {
|
let mut leader_border = Border::default();
|
||||||
let leader_color = theme.editor.replica_selection_style(*replica_id).cursor;
|
let mut leader_status_box = None;
|
||||||
let mut border = Border::all(theme.workspace.leader_border_width, leader_color);
|
if let Some(leader) = &leader {
|
||||||
border
|
let leader_color = theme
|
||||||
|
.editor
|
||||||
|
.selection_style_for_room_participant(leader.participant_index.0)
|
||||||
|
.cursor;
|
||||||
|
leader_border = Border::all(theme.workspace.leader_border_width, leader_color);
|
||||||
|
leader_border
|
||||||
.color
|
.color
|
||||||
.fade_out(1. - theme.workspace.leader_border_opacity);
|
.fade_out(1. - theme.workspace.leader_border_opacity);
|
||||||
border.overlay = true;
|
leader_border.overlay = true;
|
||||||
border
|
|
||||||
} else {
|
|
||||||
Border::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let leader_status_box = if let Some((_, leader)) = leader {
|
leader_status_box = match leader.location {
|
||||||
match leader.location {
|
|
||||||
ParticipantLocation::SharedProject {
|
ParticipantLocation::SharedProject {
|
||||||
project_id: leader_project_id,
|
project_id: leader_project_id,
|
||||||
} => {
|
} => {
|
||||||
@ -217,7 +215,6 @@ impl Member {
|
|||||||
} else {
|
} else {
|
||||||
let leader_user = leader.user.clone();
|
let leader_user = leader.user.clone();
|
||||||
let leader_user_id = leader.user.id;
|
let leader_user_id = leader.user.id;
|
||||||
let app_state = Arc::downgrade(app_state);
|
|
||||||
Some(
|
Some(
|
||||||
MouseEventHandler::new::<FollowIntoExternalProject, _>(
|
MouseEventHandler::new::<FollowIntoExternalProject, _>(
|
||||||
pane.id(),
|
pane.id(),
|
||||||
@ -241,16 +238,14 @@ impl Member {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
if let Some(app_state) = app_state.upgrade() {
|
|
||||||
crate::join_remote_project(
|
crate::join_remote_project(
|
||||||
leader_project_id,
|
leader_project_id,
|
||||||
leader_user_id,
|
leader_user_id,
|
||||||
app_state,
|
this.app_state().clone(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.aligned()
|
.aligned()
|
||||||
.bottom()
|
.bottom()
|
||||||
@ -289,13 +284,11 @@ impl Member {
|
|||||||
.right()
|
.right()
|
||||||
.into_any(),
|
.into_any(),
|
||||||
),
|
),
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Stack::new()
|
Stack::new()
|
||||||
.with_child(pane_element.contained().with_border(border))
|
.with_child(pane_element.contained().with_border(leader_border))
|
||||||
.with_children(leader_status_box)
|
.with_children(leader_status_box)
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
|
@ -375,11 +375,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
});
|
});
|
||||||
|
|
||||||
let client = &app_state.client;
|
|
||||||
client.add_view_request_handler(Workspace::handle_follow);
|
|
||||||
client.add_view_message_handler(Workspace::handle_unfollow);
|
|
||||||
client.add_view_message_handler(Workspace::handle_update_followers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectItemBuilders = HashMap<
|
type ProjectItemBuilders = HashMap<
|
||||||
@ -456,6 +451,7 @@ pub struct AppState {
|
|||||||
pub client: Arc<Client>,
|
pub client: Arc<Client>,
|
||||||
pub user_store: ModelHandle<UserStore>,
|
pub user_store: ModelHandle<UserStore>,
|
||||||
pub channel_store: ModelHandle<ChannelStore>,
|
pub channel_store: ModelHandle<ChannelStore>,
|
||||||
|
pub workspace_store: ModelHandle<WorkspaceStore>,
|
||||||
pub fs: Arc<dyn fs::Fs>,
|
pub fs: Arc<dyn fs::Fs>,
|
||||||
pub build_window_options:
|
pub build_window_options:
|
||||||
fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
|
fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
|
||||||
@ -464,6 +460,19 @@ pub struct AppState {
|
|||||||
pub background_actions: BackgroundActions,
|
pub background_actions: BackgroundActions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct WorkspaceStore {
|
||||||
|
workspaces: HashSet<WeakViewHandle<Workspace>>,
|
||||||
|
followers: Vec<Follower>,
|
||||||
|
client: Arc<Client>,
|
||||||
|
_subscriptions: Vec<client::Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||||
|
struct Follower {
|
||||||
|
project_id: Option<u64>,
|
||||||
|
peer_id: PeerId,
|
||||||
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn test(cx: &mut AppContext) -> Arc<Self> {
|
pub fn test(cx: &mut AppContext) -> Arc<Self> {
|
||||||
@ -480,6 +489,7 @@ impl AppState {
|
|||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||||
let channel_store =
|
let channel_store =
|
||||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||||
|
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||||
|
|
||||||
theme::init((), cx);
|
theme::init((), cx);
|
||||||
client::init(&client, cx);
|
client::init(&client, cx);
|
||||||
@ -491,6 +501,7 @@ impl AppState {
|
|||||||
languages,
|
languages,
|
||||||
user_store,
|
user_store,
|
||||||
channel_store,
|
channel_store,
|
||||||
|
workspace_store,
|
||||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||||
build_window_options: |_, _, _| Default::default(),
|
build_window_options: |_, _, _| Default::default(),
|
||||||
background_actions: || &[],
|
background_actions: || &[],
|
||||||
@ -551,7 +562,6 @@ pub enum Event {
|
|||||||
|
|
||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
weak_self: WeakViewHandle<Self>,
|
weak_self: WeakViewHandle<Self>,
|
||||||
remote_entity_subscription: Option<client::Subscription>,
|
|
||||||
modal: Option<ActiveModal>,
|
modal: Option<ActiveModal>,
|
||||||
zoomed: Option<AnyWeakViewHandle>,
|
zoomed: Option<AnyWeakViewHandle>,
|
||||||
zoomed_position: Option<DockPosition>,
|
zoomed_position: Option<DockPosition>,
|
||||||
@ -567,7 +577,6 @@ pub struct Workspace {
|
|||||||
titlebar_item: Option<AnyViewHandle>,
|
titlebar_item: Option<AnyViewHandle>,
|
||||||
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
leader_state: LeaderState,
|
|
||||||
follower_states_by_leader: FollowerStatesByLeader,
|
follower_states_by_leader: FollowerStatesByLeader,
|
||||||
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
|
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
|
||||||
window_edited: bool,
|
window_edited: bool,
|
||||||
@ -593,11 +602,6 @@ pub struct ViewId {
|
|||||||
pub id: u64,
|
pub id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct LeaderState {
|
|
||||||
followers: HashSet<PeerId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
|
type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -618,9 +622,8 @@ impl Workspace {
|
|||||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||||
cx.subscribe(&project, move |this, _, event, cx| {
|
cx.subscribe(&project, move |this, _, event, cx| {
|
||||||
match event {
|
match event {
|
||||||
project::Event::RemoteIdChanged(remote_id) => {
|
project::Event::RemoteIdChanged(_) => {
|
||||||
this.update_window_title(cx);
|
this.update_window_title(cx);
|
||||||
this.project_remote_id_changed(*remote_id, cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
project::Event::CollaboratorLeft(peer_id) => {
|
project::Event::CollaboratorLeft(peer_id) => {
|
||||||
@ -675,6 +678,10 @@ impl Workspace {
|
|||||||
cx.focus(¢er_pane);
|
cx.focus(¢er_pane);
|
||||||
cx.emit(Event::PaneAdded(center_pane.clone()));
|
cx.emit(Event::PaneAdded(center_pane.clone()));
|
||||||
|
|
||||||
|
app_state.workspace_store.update(cx, |store, _| {
|
||||||
|
store.workspaces.insert(weak_handle.clone());
|
||||||
|
});
|
||||||
|
|
||||||
let mut current_user = app_state.user_store.read(cx).watch_current_user();
|
let mut current_user = app_state.user_store.read(cx).watch_current_user();
|
||||||
let mut connection_status = app_state.client.status();
|
let mut connection_status = app_state.client.status();
|
||||||
let _observe_current_user = cx.spawn(|this, mut cx| async move {
|
let _observe_current_user = cx.spawn(|this, mut cx| async move {
|
||||||
@ -768,7 +775,8 @@ impl Workspace {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut this = Workspace {
|
cx.defer(|this, cx| this.update_window_title(cx));
|
||||||
|
Workspace {
|
||||||
weak_self: weak_handle.clone(),
|
weak_self: weak_handle.clone(),
|
||||||
modal: None,
|
modal: None,
|
||||||
zoomed: None,
|
zoomed: None,
|
||||||
@ -781,12 +789,10 @@ impl Workspace {
|
|||||||
status_bar,
|
status_bar,
|
||||||
titlebar_item: None,
|
titlebar_item: None,
|
||||||
notifications: Default::default(),
|
notifications: Default::default(),
|
||||||
remote_entity_subscription: None,
|
|
||||||
left_dock,
|
left_dock,
|
||||||
bottom_dock,
|
bottom_dock,
|
||||||
right_dock,
|
right_dock,
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
leader_state: Default::default(),
|
|
||||||
follower_states_by_leader: Default::default(),
|
follower_states_by_leader: Default::default(),
|
||||||
last_leaders_by_pane: Default::default(),
|
last_leaders_by_pane: Default::default(),
|
||||||
window_edited: false,
|
window_edited: false,
|
||||||
@ -799,10 +805,7 @@ impl Workspace {
|
|||||||
leader_updates_tx,
|
leader_updates_tx,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
pane_history_timestamp,
|
pane_history_timestamp,
|
||||||
};
|
}
|
||||||
this.project_remote_id_changed(project.read(cx).remote_id(), cx);
|
|
||||||
cx.defer(|this, cx| this.update_window_title(cx));
|
|
||||||
this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_local(
|
fn new_local(
|
||||||
@ -2506,24 +2509,11 @@ impl Workspace {
|
|||||||
&self.active_pane
|
&self.active_pane
|
||||||
}
|
}
|
||||||
|
|
||||||
fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
|
|
||||||
if let Some(remote_id) = remote_id {
|
|
||||||
self.remote_entity_subscription = Some(
|
|
||||||
self.app_state
|
|
||||||
.client
|
|
||||||
.add_view_for_remote_entity(remote_id, cx),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
self.remote_entity_subscription.take();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
|
fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
|
||||||
self.leader_state.followers.remove(&peer_id);
|
|
||||||
if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
|
if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
|
||||||
for state in states_by_pane.into_values() {
|
for state in states_by_pane.into_values() {
|
||||||
for item in state.items_by_leader_view_id.into_values() {
|
for item in state.items_by_leader_view_id.into_values() {
|
||||||
item.set_leader_replica_id(None, cx);
|
item.set_leader_peer_id(None, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2551,8 +2541,10 @@ impl Workspace {
|
|||||||
.insert(pane.clone(), Default::default());
|
.insert(pane.clone(), Default::default());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
let project_id = self.project.read(cx).remote_id()?;
|
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
||||||
|
let project_id = self.project.read(cx).remote_id();
|
||||||
let request = self.app_state.client.request(proto::Follow {
|
let request = self.app_state.client.request(proto::Follow {
|
||||||
|
room_id,
|
||||||
project_id,
|
project_id,
|
||||||
leader_id: Some(leader_id),
|
leader_id: Some(leader_id),
|
||||||
});
|
});
|
||||||
@ -2625,21 +2617,22 @@ impl Workspace {
|
|||||||
let leader_id = *leader_id;
|
let leader_id = *leader_id;
|
||||||
if let Some(state) = states_by_pane.remove(pane) {
|
if let Some(state) = states_by_pane.remove(pane) {
|
||||||
for (_, item) in state.items_by_leader_view_id {
|
for (_, item) in state.items_by_leader_view_id {
|
||||||
item.set_leader_replica_id(None, cx);
|
item.set_leader_peer_id(None, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if states_by_pane.is_empty() {
|
if states_by_pane.is_empty() {
|
||||||
self.follower_states_by_leader.remove(&leader_id);
|
self.follower_states_by_leader.remove(&leader_id);
|
||||||
if let Some(project_id) = self.project.read(cx).remote_id() {
|
let project_id = self.project.read(cx).remote_id();
|
||||||
|
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
||||||
self.app_state
|
self.app_state
|
||||||
.client
|
.client
|
||||||
.send(proto::Unfollow {
|
.send(proto::Unfollow {
|
||||||
|
room_id,
|
||||||
project_id,
|
project_id,
|
||||||
leader_id: Some(leader_id),
|
leader_id: Some(leader_id),
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
return Some(leader_id);
|
return Some(leader_id);
|
||||||
@ -2652,10 +2645,6 @@ impl Workspace {
|
|||||||
self.follower_states_by_leader.contains_key(&peer_id)
|
self.follower_states_by_leader.contains_key(&peer_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
|
|
||||||
self.leader_state.followers.contains(&peer_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
// TODO: There should be a better system in place for this
|
// TODO: There should be a better system in place for this
|
||||||
// (https://github.com/zed-industries/zed/issues/1290)
|
// (https://github.com/zed-industries/zed/issues/1290)
|
||||||
@ -2806,19 +2795,15 @@ impl Workspace {
|
|||||||
|
|
||||||
// RPC handlers
|
// RPC handlers
|
||||||
|
|
||||||
async fn handle_follow(
|
fn handle_follow(
|
||||||
this: WeakViewHandle<Self>,
|
&mut self,
|
||||||
envelope: TypedEnvelope<proto::Follow>,
|
follower_project_id: Option<u64>,
|
||||||
_: Arc<Client>,
|
cx: &mut ViewContext<Self>,
|
||||||
mut cx: AsyncAppContext,
|
) -> proto::FollowResponse {
|
||||||
) -> Result<proto::FollowResponse> {
|
let client = &self.app_state.client;
|
||||||
this.update(&mut cx, |this, cx| {
|
let project_id = self.project.read(cx).remote_id();
|
||||||
let client = &this.app_state.client;
|
|
||||||
this.leader_state
|
|
||||||
.followers
|
|
||||||
.insert(envelope.original_sender_id()?);
|
|
||||||
|
|
||||||
let active_view_id = this.active_item(cx).and_then(|i| {
|
let active_view_id = self.active_item(cx).and_then(|i| {
|
||||||
Some(
|
Some(
|
||||||
i.to_followable_item_handle(cx)?
|
i.to_followable_item_handle(cx)?
|
||||||
.remote_id(client, cx)?
|
.remote_id(client, cx)?
|
||||||
@ -2828,17 +2813,23 @@ impl Workspace {
|
|||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
Ok(proto::FollowResponse {
|
proto::FollowResponse {
|
||||||
active_view_id,
|
active_view_id,
|
||||||
views: this
|
views: self
|
||||||
.panes()
|
.panes()
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|pane| {
|
.flat_map(|pane| {
|
||||||
let leader_id = this.leader_for_pane(pane);
|
let leader_id = self.leader_for_pane(pane);
|
||||||
pane.read(cx).items().filter_map({
|
pane.read(cx).items().filter_map({
|
||||||
let cx = &cx;
|
let cx = &cx;
|
||||||
move |item| {
|
move |item| {
|
||||||
let item = item.to_followable_item_handle(cx)?;
|
let item = item.to_followable_item_handle(cx)?;
|
||||||
|
if project_id.is_some()
|
||||||
|
&& project_id != follower_project_id
|
||||||
|
&& item.is_project_item(cx)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let id = item.remote_id(client, cx)?.to_proto();
|
let id = item.remote_id(client, cx)?.to_proto();
|
||||||
let variant = item.to_state_proto(cx)?;
|
let variant = item.to_state_proto(cx)?;
|
||||||
Some(proto::View {
|
Some(proto::View {
|
||||||
@ -2850,37 +2841,18 @@ impl Workspace {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
})
|
}
|
||||||
})?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_unfollow(
|
fn handle_update_followers(
|
||||||
this: WeakViewHandle<Self>,
|
&mut self,
|
||||||
envelope: TypedEnvelope<proto::Unfollow>,
|
leader_id: PeerId,
|
||||||
_: Arc<Client>,
|
message: proto::UpdateFollowers,
|
||||||
mut cx: AsyncAppContext,
|
_cx: &mut ViewContext<Self>,
|
||||||
) -> Result<()> {
|
) {
|
||||||
this.update(&mut cx, |this, cx| {
|
self.leader_updates_tx
|
||||||
this.leader_state
|
.unbounded_send((leader_id, message))
|
||||||
.followers
|
.ok();
|
||||||
.remove(&envelope.original_sender_id()?);
|
|
||||||
cx.notify();
|
|
||||||
Ok(())
|
|
||||||
})?
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_update_followers(
|
|
||||||
this: WeakViewHandle<Self>,
|
|
||||||
envelope: TypedEnvelope<proto::UpdateFollowers>,
|
|
||||||
_: Arc<Client>,
|
|
||||||
cx: AsyncAppContext,
|
|
||||||
) -> Result<()> {
|
|
||||||
let leader_id = envelope.original_sender_id()?;
|
|
||||||
this.read_with(&cx, |this, _| {
|
|
||||||
this.leader_updates_tx
|
|
||||||
.unbounded_send((leader_id, envelope.payload))
|
|
||||||
})??;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn process_leader_update(
|
async fn process_leader_update(
|
||||||
@ -2953,18 +2925,6 @@ impl Workspace {
|
|||||||
let this = this
|
let this = this
|
||||||
.upgrade(cx)
|
.upgrade(cx)
|
||||||
.ok_or_else(|| anyhow!("workspace dropped"))?;
|
.ok_or_else(|| anyhow!("workspace dropped"))?;
|
||||||
let project = this
|
|
||||||
.read_with(cx, |this, _| this.project.clone())
|
|
||||||
.ok_or_else(|| anyhow!("window dropped"))?;
|
|
||||||
|
|
||||||
let replica_id = project
|
|
||||||
.read_with(cx, |project, _| {
|
|
||||||
project
|
|
||||||
.collaborators()
|
|
||||||
.get(&leader_id)
|
|
||||||
.map(|c| c.replica_id)
|
|
||||||
})
|
|
||||||
.ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
|
|
||||||
|
|
||||||
let item_builders = cx.update(|cx| {
|
let item_builders = cx.update(|cx| {
|
||||||
cx.default_global::<FollowableItemBuilders>()
|
cx.default_global::<FollowableItemBuilders>()
|
||||||
@ -3009,7 +2969,7 @@ impl Workspace {
|
|||||||
.get_mut(&pane)?;
|
.get_mut(&pane)?;
|
||||||
|
|
||||||
for (id, item) in leader_view_ids.into_iter().zip(items) {
|
for (id, item) in leader_view_ids.into_iter().zip(items) {
|
||||||
item.set_leader_replica_id(Some(replica_id), cx);
|
item.set_leader_peer_id(Some(leader_id), cx);
|
||||||
state.items_by_leader_view_id.insert(id, item);
|
state.items_by_leader_view_id.insert(id, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3020,46 +2980,44 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn update_active_view_for_followers(&self, cx: &AppContext) {
|
fn update_active_view_for_followers(&self, cx: &AppContext) {
|
||||||
|
let mut is_project_item = true;
|
||||||
|
let mut update = proto::UpdateActiveView::default();
|
||||||
if self.active_pane.read(cx).has_focus() {
|
if self.active_pane.read(cx).has_focus() {
|
||||||
self.update_followers(
|
let item = self
|
||||||
proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
|
.active_item(cx)
|
||||||
id: self.active_item(cx).and_then(|item| {
|
.and_then(|item| item.to_followable_item_handle(cx));
|
||||||
item.to_followable_item_handle(cx)?
|
if let Some(item) = item {
|
||||||
|
is_project_item = item.is_project_item(cx);
|
||||||
|
update = proto::UpdateActiveView {
|
||||||
|
id: item
|
||||||
.remote_id(&self.app_state.client, cx)
|
.remote_id(&self.app_state.client, cx)
|
||||||
.map(|id| id.to_proto())
|
.map(|id| id.to_proto()),
|
||||||
}),
|
|
||||||
leader_id: self.leader_for_pane(&self.active_pane),
|
leader_id: self.leader_for_pane(&self.active_pane),
|
||||||
}),
|
};
|
||||||
cx,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
self.update_followers(
|
|
||||||
proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
|
|
||||||
id: None,
|
|
||||||
leader_id: None,
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.update_followers(
|
||||||
|
is_project_item,
|
||||||
|
proto::update_followers::Variant::UpdateActiveView(update),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn update_followers(
|
fn update_followers(
|
||||||
&self,
|
&self,
|
||||||
|
project_only: bool,
|
||||||
update: proto::update_followers::Variant,
|
update: proto::update_followers::Variant,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let project_id = self.project.read(cx).remote_id()?;
|
let project_id = if project_only {
|
||||||
if !self.leader_state.followers.is_empty() {
|
self.project.read(cx).remote_id()
|
||||||
self.app_state
|
} else {
|
||||||
.client
|
|
||||||
.send(proto::UpdateFollowers {
|
|
||||||
project_id,
|
|
||||||
follower_ids: self.leader_state.followers.iter().copied().collect(),
|
|
||||||
variant: Some(update),
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
None
|
None
|
||||||
|
};
|
||||||
|
self.app_state().workspace_store.read_with(cx, |store, cx| {
|
||||||
|
store.update_followers(project_id, update, cx)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
|
pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
|
||||||
@ -3081,32 +3039,40 @@ impl Workspace {
|
|||||||
let room = call.read(cx).room()?.read(cx);
|
let room = call.read(cx).room()?.read(cx);
|
||||||
let participant = room.remote_participant_for_peer_id(leader_id)?;
|
let participant = room.remote_participant_for_peer_id(leader_id)?;
|
||||||
let mut items_to_activate = Vec::new();
|
let mut items_to_activate = Vec::new();
|
||||||
|
|
||||||
|
let leader_in_this_app;
|
||||||
|
let leader_in_this_project;
|
||||||
match participant.location {
|
match participant.location {
|
||||||
call::ParticipantLocation::SharedProject { project_id } => {
|
call::ParticipantLocation::SharedProject { project_id } => {
|
||||||
if Some(project_id) == self.project.read(cx).remote_id() {
|
leader_in_this_app = true;
|
||||||
for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
|
leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
|
||||||
if let Some(item) = state
|
|
||||||
.active_view_id
|
|
||||||
.and_then(|id| state.items_by_leader_view_id.get(&id))
|
|
||||||
{
|
|
||||||
items_to_activate.push((pane.clone(), item.boxed_clone()));
|
|
||||||
} else if let Some(shared_screen) =
|
|
||||||
self.shared_screen_for_peer(leader_id, pane, cx)
|
|
||||||
{
|
|
||||||
items_to_activate.push((pane.clone(), Box::new(shared_screen)));
|
|
||||||
}
|
}
|
||||||
|
call::ParticipantLocation::UnsharedProject => {
|
||||||
|
leader_in_this_app = true;
|
||||||
|
leader_in_this_project = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
call::ParticipantLocation::UnsharedProject => {}
|
|
||||||
call::ParticipantLocation::External => {
|
call::ParticipantLocation::External => {
|
||||||
for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
|
leader_in_this_app = false;
|
||||||
|
leader_in_this_project = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
|
||||||
|
if leader_in_this_app {
|
||||||
|
let item = state
|
||||||
|
.active_view_id
|
||||||
|
.and_then(|id| state.items_by_leader_view_id.get(&id));
|
||||||
|
if let Some(item) = item {
|
||||||
|
if leader_in_this_project || !item.is_project_item(cx) {
|
||||||
|
items_to_activate.push((pane.clone(), item.boxed_clone()));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
|
if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
|
||||||
items_to_activate.push((pane.clone(), Box::new(shared_screen)));
|
items_to_activate.push((pane.clone(), Box::new(shared_screen)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (pane, item) in items_to_activate {
|
for (pane, item) in items_to_activate {
|
||||||
let pane_was_focused = pane.read(cx).has_focus();
|
let pane_was_focused = pane.read(cx).has_focus();
|
||||||
@ -3149,6 +3115,7 @@ impl Workspace {
|
|||||||
|
|
||||||
pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||||
if active {
|
if active {
|
||||||
|
self.update_active_view_for_followers(cx);
|
||||||
cx.background()
|
cx.background()
|
||||||
.spawn(persistence::DB.update_timestamp(self.database_id()))
|
.spawn(persistence::DB.update_timestamp(self.database_id()))
|
||||||
.detach();
|
.detach();
|
||||||
@ -3522,8 +3489,10 @@ impl Workspace {
|
|||||||
|
|
||||||
let channel_store =
|
let channel_store =
|
||||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||||
|
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||||
let app_state = Arc::new(AppState {
|
let app_state = Arc::new(AppState {
|
||||||
languages: project.read(cx).languages().clone(),
|
languages: project.read(cx).languages().clone(),
|
||||||
|
workspace_store,
|
||||||
client,
|
client,
|
||||||
user_store,
|
user_store,
|
||||||
channel_store,
|
channel_store,
|
||||||
@ -3767,6 +3736,12 @@ fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut Asy
|
|||||||
|
|
||||||
impl Entity for Workspace {
|
impl Entity for Workspace {
|
||||||
type Event = Event;
|
type Event = Event;
|
||||||
|
|
||||||
|
fn release(&mut self, cx: &mut AppContext) {
|
||||||
|
self.app_state.workspace_store.update(cx, |store, _| {
|
||||||
|
store.workspaces.remove(&self.weak_self);
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl View for Workspace {
|
impl View for Workspace {
|
||||||
@ -3909,6 +3884,151 @@ impl View for Workspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WorkspaceStore {
|
||||||
|
pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
workspaces: Default::default(),
|
||||||
|
followers: Default::default(),
|
||||||
|
_subscriptions: vec![
|
||||||
|
client.add_request_handler(cx.handle(), Self::handle_follow),
|
||||||
|
client.add_message_handler(cx.handle(), Self::handle_unfollow),
|
||||||
|
client.add_message_handler(cx.handle(), Self::handle_update_followers),
|
||||||
|
],
|
||||||
|
client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_followers(
|
||||||
|
&self,
|
||||||
|
project_id: Option<u64>,
|
||||||
|
update: proto::update_followers::Variant,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Option<()> {
|
||||||
|
if !cx.has_global::<ModelHandle<ActiveCall>>() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
|
||||||
|
let follower_ids: Vec<_> = self
|
||||||
|
.followers
|
||||||
|
.iter()
|
||||||
|
.filter_map(|follower| {
|
||||||
|
if follower.project_id == project_id || project_id.is_none() {
|
||||||
|
Some(follower.peer_id.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if follower_ids.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.client
|
||||||
|
.send(proto::UpdateFollowers {
|
||||||
|
room_id,
|
||||||
|
project_id,
|
||||||
|
follower_ids,
|
||||||
|
variant: Some(update),
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_follow(
|
||||||
|
this: ModelHandle<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::Follow>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::FollowResponse> {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let follower = Follower {
|
||||||
|
project_id: envelope.payload.project_id,
|
||||||
|
peer_id: envelope.original_sender_id()?,
|
||||||
|
};
|
||||||
|
let active_project = ActiveCall::global(cx)
|
||||||
|
.read(cx)
|
||||||
|
.location()
|
||||||
|
.map(|project| project.id());
|
||||||
|
|
||||||
|
let mut response = proto::FollowResponse::default();
|
||||||
|
for workspace in &this.workspaces {
|
||||||
|
let Some(workspace) = workspace.upgrade(cx) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
workspace.update(cx.as_mut(), |workspace, cx| {
|
||||||
|
let handler_response = workspace.handle_follow(follower.project_id, cx);
|
||||||
|
if response.views.is_empty() {
|
||||||
|
response.views = handler_response.views;
|
||||||
|
} else {
|
||||||
|
response.views.extend_from_slice(&handler_response.views);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(active_view_id) = handler_response.active_view_id.clone() {
|
||||||
|
if response.active_view_id.is_none()
|
||||||
|
|| Some(workspace.project.id()) == active_project
|
||||||
|
{
|
||||||
|
response.active_view_id = Some(active_view_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(ix) = this.followers.binary_search(&follower) {
|
||||||
|
this.followers.insert(ix, follower);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_unfollow(
|
||||||
|
this: ModelHandle<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::Unfollow>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
let follower = Follower {
|
||||||
|
project_id: envelope.payload.project_id,
|
||||||
|
peer_id: envelope.original_sender_id()?,
|
||||||
|
};
|
||||||
|
if let Ok(ix) = this.followers.binary_search(&follower) {
|
||||||
|
this.followers.remove(ix);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_update_followers(
|
||||||
|
this: ModelHandle<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::UpdateFollowers>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let leader_id = envelope.original_sender_id()?;
|
||||||
|
let update = envelope.payload;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
for workspace in &this.workspaces {
|
||||||
|
let Some(workspace) = workspace.upgrade(cx) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
workspace.update(cx.as_mut(), |workspace, cx| {
|
||||||
|
let project_id = workspace.project.read(cx).remote_id();
|
||||||
|
if update.project_id != project_id && update.project_id.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
workspace.handle_update_followers(leader_id, update.clone(), cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for WorkspaceStore {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
impl ViewId {
|
impl ViewId {
|
||||||
pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
|
pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
@ -54,7 +54,7 @@ use welcome::{show_welcome_experience, FIRST_OPEN};
|
|||||||
|
|
||||||
use fs::RealFs;
|
use fs::RealFs;
|
||||||
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
|
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
|
||||||
use workspace::AppState;
|
use workspace::{AppState, WorkspaceStore};
|
||||||
use zed::{
|
use zed::{
|
||||||
assets::Assets,
|
assets::Assets,
|
||||||
build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
|
build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
|
||||||
@ -139,6 +139,7 @@ fn main() {
|
|||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
|
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
|
||||||
let channel_store =
|
let channel_store =
|
||||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||||
|
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||||
|
|
||||||
cx.set_global(client.clone());
|
cx.set_global(client.clone());
|
||||||
|
|
||||||
@ -187,6 +188,7 @@ fn main() {
|
|||||||
build_window_options,
|
build_window_options,
|
||||||
initialize_workspace,
|
initialize_workspace,
|
||||||
background_actions,
|
background_actions,
|
||||||
|
workspace_store,
|
||||||
});
|
});
|
||||||
cx.set_global(Arc::downgrade(&app_state));
|
cx.set_global(Arc::downgrade(&app_state));
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ position_2=${half_width},${y}
|
|||||||
|
|
||||||
# Authenticate using the collab server's admin secret.
|
# Authenticate using the collab server's admin secret.
|
||||||
export ZED_STATELESS=1
|
export ZED_STATELESS=1
|
||||||
|
export ZED_ALWAYS_ACTIVE=1
|
||||||
export ZED_ADMIN_API_TOKEN=secret
|
export ZED_ADMIN_API_TOKEN=secret
|
||||||
export ZED_SERVER_URL=http://localhost:8080
|
export ZED_SERVER_URL=http://localhost:8080
|
||||||
export ZED_WINDOW_SIZE=${half_width},${height}
|
export ZED_WINDOW_SIZE=${half_width},${height}
|
||||||
|
Loading…
Reference in New Issue
Block a user