Merge branch 'main' into project_search_design
This commit is contained in:
commit
afebe3faf8
133 changed files with 9714 additions and 2998 deletions
|
@ -192,6 +192,7 @@ impl AssistantPanel {
|
|||
old_dock_position = new_dock_position;
|
||||
cx.emit(AssistantPanelEvent::DockPositionChanged);
|
||||
}
|
||||
cx.notify();
|
||||
})];
|
||||
|
||||
this
|
||||
|
@ -725,10 +726,10 @@ impl Panel for AssistantPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
|
||||
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
|
||||
match self.position(cx) {
|
||||
DockPosition::Left | DockPosition::Right => self.width = Some(size),
|
||||
DockPosition::Bottom => self.height = Some(size),
|
||||
DockPosition::Left | DockPosition::Right => self.width = size,
|
||||
DockPosition::Bottom => self.height = size,
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -780,8 +781,10 @@ impl Panel for AssistantPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn icon_path(&self) -> &'static str {
|
||||
"icons/robot_14.svg"
|
||||
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
|
||||
settings::get::<AssistantSettings>(cx)
|
||||
.button
|
||||
.then(|| "icons/ai.svg")
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
|
||||
|
|
|
@ -13,6 +13,7 @@ pub enum AssistantDockPosition {
|
|||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AssistantSettings {
|
||||
pub button: bool,
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: f32,
|
||||
pub default_height: f32,
|
||||
|
@ -20,6 +21,7 @@ pub struct AssistantSettings {
|
|||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContent {
|
||||
pub button: Option<bool>,
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
pub default_height: Option<f32>,
|
||||
|
|
|
@ -39,29 +39,43 @@ pub struct Audio {
|
|||
|
||||
impl Audio {
|
||||
pub fn new() -> Self {
|
||||
let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
|
||||
|
||||
Self {
|
||||
_output_stream,
|
||||
output_handle,
|
||||
_output_stream: None,
|
||||
output_handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_sound(sound: Sound, cx: &AppContext) {
|
||||
fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> {
|
||||
if self.output_handle.is_none() {
|
||||
let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
|
||||
self.output_handle = output_handle;
|
||||
self._output_stream = _output_stream;
|
||||
}
|
||||
|
||||
self.output_handle.as_ref()
|
||||
}
|
||||
|
||||
pub fn play_sound(sound: Sound, cx: &mut AppContext) {
|
||||
if !cx.has_global::<Self>() {
|
||||
return;
|
||||
}
|
||||
|
||||
let this = cx.global::<Self>();
|
||||
cx.update_global::<Self, _, _>(|this, cx| {
|
||||
let output_handle = this.ensure_output_exists()?;
|
||||
let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
|
||||
output_handle.play_raw(source).log_err()?;
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
let Some(output_handle) = this.output_handle.as_ref() else {
|
||||
pub fn end_call(cx: &mut AppContext) {
|
||||
if !cx.has_global::<Self>() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
output_handle.play_raw(source).log_err();
|
||||
cx.update_global::<Self, _, _>(|this, _| {
|
||||
this._output_stream.take();
|
||||
this.output_handle.take();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,11 @@ pub mod room;
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use audio::Audio;
|
||||
use call_settings::CallSettings;
|
||||
use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
|
||||
use client::{
|
||||
proto, ChannelId, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
|
||||
};
|
||||
use collections::HashSet;
|
||||
use futures::{future::Shared, FutureExt};
|
||||
use postage::watch;
|
||||
|
@ -75,6 +78,10 @@ impl ActiveCall {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
|
||||
self.room()?.read(cx).channel_id()
|
||||
}
|
||||
|
||||
async fn handle_incoming_call(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::IncomingCall>,
|
||||
|
@ -274,9 +281,36 @@ impl ActiveCall {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn join_channel(
|
||||
&mut self,
|
||||
channel_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if let Some(room) = self.room().cloned() {
|
||||
if room.read(cx).channel_id() == Some(channel_id) {
|
||||
return Task::ready(Ok(()));
|
||||
} else {
|
||||
room.update(cx, |room, cx| room.clear_state(cx));
|
||||
}
|
||||
}
|
||||
|
||||
let join = Room::join_channel(channel_id, self.client.clone(), self.user_store.clone(), cx);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let room = join.await?;
|
||||
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("join channel", cx)
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
self.report_call_event("hang up", cx);
|
||||
Audio::end_call(cx);
|
||||
if let Some((room, _)) = self.room.take() {
|
||||
room.update(cx, |room, cx| room.leave(cx))
|
||||
} else {
|
||||
|
|
|
@ -49,6 +49,7 @@ pub enum Event {
|
|||
|
||||
pub struct Room {
|
||||
id: u64,
|
||||
channel_id: Option<u64>,
|
||||
live_kit: Option<LiveKitRoom>,
|
||||
status: RoomStatus,
|
||||
shared_projects: HashSet<WeakModelHandle<Project>>,
|
||||
|
@ -93,8 +94,25 @@ impl Entity for Room {
|
|||
}
|
||||
|
||||
impl Room {
|
||||
pub fn channel_id(&self) -> Option<u64> {
|
||||
self.channel_id
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn is_connected(&self) -> bool {
|
||||
if let Some(live_kit) = self.live_kit.as_ref() {
|
||||
matches!(
|
||||
*live_kit.room.status().borrow(),
|
||||
live_kit_client::ConnectionState::Connected { .. }
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn new(
|
||||
id: u64,
|
||||
channel_id: Option<u64>,
|
||||
live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
|
@ -185,6 +203,7 @@ impl Room {
|
|||
|
||||
Self {
|
||||
id,
|
||||
channel_id,
|
||||
live_kit: live_kit_room,
|
||||
status: RoomStatus::Online,
|
||||
shared_projects: Default::default(),
|
||||
|
@ -217,6 +236,7 @@ impl Room {
|
|||
let room = cx.add_model(|cx| {
|
||||
Self::new(
|
||||
room_proto.id,
|
||||
None,
|
||||
response.live_kit_connection_info,
|
||||
client,
|
||||
user_store,
|
||||
|
@ -248,35 +268,64 @@ impl Room {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn join_channel(
|
||||
channel_id: u64,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<ModelHandle<Self>>> {
|
||||
cx.spawn(|cx| async move {
|
||||
Self::from_join_response(
|
||||
client.request(proto::JoinChannel { channel_id }).await?,
|
||||
client,
|
||||
user_store,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn join(
|
||||
call: &IncomingCall,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<ModelHandle<Self>>> {
|
||||
let room_id = call.room_id;
|
||||
cx.spawn(|mut cx| async move {
|
||||
let response = client.request(proto::JoinRoom { id: room_id }).await?;
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
let room = cx.add_model(|cx| {
|
||||
Self::new(
|
||||
room_id,
|
||||
response.live_kit_connection_info,
|
||||
client,
|
||||
user_store,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.leave_when_empty = true;
|
||||
room.apply_room_update(room_proto, cx)?;
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
|
||||
Ok(room)
|
||||
let id = call.room_id;
|
||||
cx.spawn(|cx| async move {
|
||||
Self::from_join_response(
|
||||
client.request(proto::JoinRoom { id }).await?,
|
||||
client,
|
||||
user_store,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn from_join_response(
|
||||
response: proto::JoinRoomResponse,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<ModelHandle<Self>> {
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
let room = cx.add_model(|cx| {
|
||||
Self::new(
|
||||
room_proto.id,
|
||||
response.channel_id,
|
||||
response.live_kit_connection_info,
|
||||
client,
|
||||
user_store,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.leave_when_empty = room.channel_id.is_none();
|
||||
room.apply_room_update(room_proto, cx)?;
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
fn should_leave(&self) -> bool {
|
||||
self.leave_when_empty
|
||||
&& self.pending_room_update.is_none()
|
||||
|
@ -297,7 +346,18 @@ impl Room {
|
|||
}
|
||||
|
||||
log::info!("leaving room");
|
||||
Audio::play_sound(Sound::Leave, cx);
|
||||
|
||||
self.clear_state(cx);
|
||||
|
||||
let leave_room = self.client.request(proto::LeaveRoom {});
|
||||
cx.background().spawn(async move {
|
||||
leave_room.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn clear_state(&mut self, cx: &mut AppContext) {
|
||||
for project in self.shared_projects.drain() {
|
||||
if let Some(project) = project.upgrade(cx) {
|
||||
project.update(cx, |project, cx| {
|
||||
|
@ -314,8 +374,6 @@ impl Room {
|
|||
}
|
||||
}
|
||||
|
||||
Audio::play_sound(Sound::Leave, cx);
|
||||
|
||||
self.status = RoomStatus::Offline;
|
||||
self.remote_participants.clear();
|
||||
self.pending_participants.clear();
|
||||
|
@ -324,12 +382,6 @@ impl Room {
|
|||
self.live_kit.take();
|
||||
self.pending_room_update.take();
|
||||
self.maintain_connection.take();
|
||||
|
||||
let leave_room = self.client.request(proto::LeaveRoom {});
|
||||
cx.background().spawn(async move {
|
||||
leave_room.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn maintain_connection(
|
||||
|
@ -1066,11 +1118,11 @@ impl Room {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn is_muted(&self) -> bool {
|
||||
pub fn is_muted(&self, cx: &AppContext) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
.and_then(|live_kit| match &live_kit.microphone_track {
|
||||
LocalTrack::None => Some(true),
|
||||
LocalTrack::None => Some(settings::get::<CallSettings>(cx).mute_on_join),
|
||||
LocalTrack::Pending { muted, .. } => Some(*muted),
|
||||
LocalTrack::Published { muted, .. } => Some(*muted),
|
||||
})
|
||||
|
@ -1260,7 +1312,7 @@ impl Room {
|
|||
}
|
||||
|
||||
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
|
||||
let should_mute = !self.is_muted();
|
||||
let should_mute = !self.is_muted(cx);
|
||||
if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
if matches!(live_kit.microphone_track, LocalTrack::None) {
|
||||
return Ok(self.share_microphone(cx));
|
||||
|
|
550
crates/client/src/channel_store.rs
Normal file
550
crates/client/src/channel_store.rs
Normal file
|
@ -0,0 +1,550 @@
|
|||
use crate::Status;
|
||||
use crate::{Client, Subscription, User, UserStore};
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use collections::HashSet;
|
||||
use futures::channel::mpsc;
|
||||
use futures::Future;
|
||||
use futures::StreamExt;
|
||||
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
|
||||
use rpc::{proto, TypedEnvelope};
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
pub type ChannelId = u64;
|
||||
pub type UserId = u64;
|
||||
|
||||
pub struct ChannelStore {
|
||||
channels_by_id: HashMap<ChannelId, Arc<Channel>>,
|
||||
channel_paths: Vec<Vec<ChannelId>>,
|
||||
channel_invitations: Vec<Arc<Channel>>,
|
||||
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
||||
channels_with_admin_privileges: HashSet<ChannelId>,
|
||||
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
||||
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
_rpc_subscription: Subscription,
|
||||
_watch_connection_status: Task<()>,
|
||||
_update_channels: Task<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub struct ChannelMembership {
|
||||
pub user: Arc<User>,
|
||||
pub kind: proto::channel_member::Kind,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
pub enum ChannelEvent {
|
||||
ChannelCreated(ChannelId),
|
||||
ChannelRenamed(ChannelId),
|
||||
}
|
||||
|
||||
impl Entity for ChannelStore {
|
||||
type Event = ChannelEvent;
|
||||
}
|
||||
|
||||
pub enum ChannelMemberStatus {
|
||||
Invited,
|
||||
Member,
|
||||
NotMember,
|
||||
}
|
||||
|
||||
impl ChannelStore {
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let rpc_subscription =
|
||||
client.add_message_handler(cx.handle(), Self::handle_update_channels);
|
||||
|
||||
let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
|
||||
let mut connection_status = client.status();
|
||||
let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some(status) = connection_status.next().await {
|
||||
if matches!(status, Status::ConnectionLost | Status::SignedOut) {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.channels_by_id.clear();
|
||||
this.channel_invitations.clear();
|
||||
this.channel_participants.clear();
|
||||
this.channels_with_admin_privileges.clear();
|
||||
this.channel_paths.clear();
|
||||
this.outgoing_invites.clear();
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Self {
|
||||
channels_by_id: HashMap::default(),
|
||||
channel_invitations: Vec::default(),
|
||||
channel_paths: Vec::default(),
|
||||
channel_participants: Default::default(),
|
||||
channels_with_admin_privileges: Default::default(),
|
||||
outgoing_invites: Default::default(),
|
||||
update_channels_tx,
|
||||
client,
|
||||
user_store,
|
||||
_rpc_subscription: rpc_subscription,
|
||||
_watch_connection_status: watch_connection_status,
|
||||
_update_channels: cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some(update_channels) = update_channels_rx.next().await {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
let update_task = this.update(&mut cx, |this, cx| {
|
||||
this.update_channels(update_channels, cx)
|
||||
});
|
||||
if let Some(update_task) = update_task {
|
||||
update_task.await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel_count(&self) -> usize {
|
||||
self.channel_paths.len()
|
||||
}
|
||||
|
||||
pub fn channels(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
|
||||
self.channel_paths.iter().map(move |path| {
|
||||
let id = path.last().unwrap();
|
||||
let channel = self.channel_for_id(*id).unwrap();
|
||||
(path.len() - 1, channel)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc<Channel>)> {
|
||||
let path = self.channel_paths.get(ix)?;
|
||||
let id = path.last().unwrap();
|
||||
let channel = self.channel_for_id(*id).unwrap();
|
||||
Some((path.len() - 1, channel))
|
||||
}
|
||||
|
||||
pub fn channel_invitations(&self) -> &[Arc<Channel>] {
|
||||
&self.channel_invitations
|
||||
}
|
||||
|
||||
pub fn channel_for_id(&self, channel_id: ChannelId) -> Option<&Arc<Channel>> {
|
||||
self.channels_by_id.get(&channel_id)
|
||||
}
|
||||
|
||||
pub fn is_user_admin(&self, channel_id: ChannelId) -> bool {
|
||||
self.channel_paths.iter().any(|path| {
|
||||
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
|
||||
path[..=ix]
|
||||
.iter()
|
||||
.any(|id| self.channels_with_admin_privileges.contains(id))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc<User>] {
|
||||
self.channel_participants
|
||||
.get(&channel_id)
|
||||
.map_or(&[], |v| v.as_slice())
|
||||
}
|
||||
|
||||
pub fn create_channel(
|
||||
&self,
|
||||
name: &str,
|
||||
parent_id: Option<ChannelId>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ChannelId>> {
|
||||
let client = self.client.clone();
|
||||
let name = name.trim_start_matches("#").to_owned();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let channel = client
|
||||
.request(proto::CreateChannel { name, parent_id })
|
||||
.await?
|
||||
.channel
|
||||
.ok_or_else(|| anyhow!("missing channel in response"))?;
|
||||
|
||||
let channel_id = channel.id;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let task = this.update_channels(
|
||||
proto::UpdateChannels {
|
||||
channels: vec![channel],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
assert!(task.is_none());
|
||||
|
||||
// This event is emitted because the collab panel wants to clear the pending edit state
|
||||
// before this frame is rendered. But we can't guarantee that the collab panel's future
|
||||
// will resolve before this flush_effects finishes. Synchronously emitting this event
|
||||
// ensures that the collab panel will observe this creation before the frame completes
|
||||
cx.emit(ChannelEvent::ChannelCreated(channel_id));
|
||||
});
|
||||
|
||||
Ok(channel_id)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn invite_member(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
return Task::ready(Err(anyhow!("invite request already in progress")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
let client = self.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::InviteChannelMember {
|
||||
channel_id,
|
||||
user_id,
|
||||
admin,
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.outgoing_invites.remove(&(channel_id, user_id));
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
result?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove_member(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
return Task::ready(Err(anyhow!("invite request already in progress")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
let client = self.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::RemoveChannelMember {
|
||||
channel_id,
|
||||
user_id,
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.outgoing_invites.remove(&(channel_id, user_id));
|
||||
cx.notify();
|
||||
});
|
||||
result?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_member_admin(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
return Task::ready(Err(anyhow!("member request already in progress")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
let client = self.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::SetChannelMemberAdmin {
|
||||
channel_id,
|
||||
user_id,
|
||||
admin,
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.outgoing_invites.remove(&(channel_id, user_id));
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
result?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rename(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
new_name: &str,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
let name = new_name.to_string();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let channel = client
|
||||
.request(proto::RenameChannel { channel_id, name })
|
||||
.await?
|
||||
.channel
|
||||
.ok_or_else(|| anyhow!("missing channel in response"))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let task = this.update_channels(
|
||||
proto::UpdateChannels {
|
||||
channels: vec![channel],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
assert!(task.is_none());
|
||||
|
||||
// This event is emitted because the collab panel wants to clear the pending edit state
|
||||
// before this frame is rendered. But we can't guarantee that the collab panel's future
|
||||
// will resolve before this flush_effects finishes. Synchronously emitting this event
|
||||
// ensures that the collab panel will observe this creation before the frame complete
|
||||
cx.emit(ChannelEvent::ChannelRenamed(channel_id))
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn respond_to_channel_invite(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
accept: bool,
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
let client = self.client.clone();
|
||||
async move {
|
||||
client
|
||||
.request(proto::RespondToChannelInvite { channel_id, accept })
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_channel_member_details(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<ChannelMembership>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.downgrade();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let response = client
|
||||
.request(proto::GetChannelMembers { channel_id })
|
||||
.await?;
|
||||
|
||||
let user_ids = response.members.iter().map(|m| m.user_id).collect();
|
||||
let user_store = user_store
|
||||
.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("user store dropped"))?;
|
||||
let users = user_store
|
||||
.update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))
|
||||
.await?;
|
||||
|
||||
Ok(users
|
||||
.into_iter()
|
||||
.zip(response.members)
|
||||
.filter_map(|(user, member)| {
|
||||
Some(ChannelMembership {
|
||||
user,
|
||||
admin: member.admin,
|
||||
kind: proto::channel_member::Kind::from_i32(member.kind)?,
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future<Output = Result<()>> {
|
||||
let client = self.client.clone();
|
||||
async move {
|
||||
client.request(proto::RemoveChannel { channel_id }).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_pending_channel_invite_response(&self, _: &Arc<Channel>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn has_pending_channel_invite(&self, channel_id: ChannelId, user_id: UserId) -> bool {
|
||||
self.outgoing_invites.contains(&(channel_id, user_id))
|
||||
}
|
||||
|
||||
async fn handle_update_channels(
|
||||
this: ModelHandle<Self>,
|
||||
message: TypedEnvelope<proto::UpdateChannels>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.update_channels_tx
|
||||
.unbounded_send(message.payload)
|
||||
.unwrap();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn update_channels(
|
||||
&mut self,
|
||||
payload: proto::UpdateChannels,
|
||||
cx: &mut ModelContext<ChannelStore>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if !payload.remove_channel_invitations.is_empty() {
|
||||
self.channel_invitations
|
||||
.retain(|channel| !payload.remove_channel_invitations.contains(&channel.id));
|
||||
}
|
||||
for channel in payload.channel_invitations {
|
||||
match self
|
||||
.channel_invitations
|
||||
.binary_search_by_key(&channel.id, |c| c.id)
|
||||
{
|
||||
Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name,
|
||||
Err(ix) => self.channel_invitations.insert(
|
||||
ix,
|
||||
Arc::new(Channel {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
let channels_changed = !payload.channels.is_empty() || !payload.remove_channels.is_empty();
|
||||
if channels_changed {
|
||||
if !payload.remove_channels.is_empty() {
|
||||
self.channels_by_id
|
||||
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
|
||||
self.channel_participants
|
||||
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
|
||||
self.channels_with_admin_privileges
|
||||
.retain(|channel_id| !payload.remove_channels.contains(channel_id));
|
||||
}
|
||||
|
||||
for channel in payload.channels {
|
||||
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel.id) {
|
||||
// FIXME: We may be missing a path for this existing channel in certain cases
|
||||
let existing_channel = Arc::make_mut(existing_channel);
|
||||
existing_channel.name = channel.name;
|
||||
continue;
|
||||
}
|
||||
|
||||
self.channels_by_id.insert(
|
||||
channel.id,
|
||||
Arc::new(Channel {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
}),
|
||||
);
|
||||
|
||||
if let Some(parent_id) = channel.parent_id {
|
||||
let mut ix = 0;
|
||||
while ix < self.channel_paths.len() {
|
||||
let path = &self.channel_paths[ix];
|
||||
if path.ends_with(&[parent_id]) {
|
||||
let mut new_path = path.clone();
|
||||
new_path.push(channel.id);
|
||||
self.channel_paths.insert(ix + 1, new_path);
|
||||
ix += 1;
|
||||
}
|
||||
ix += 1;
|
||||
}
|
||||
} else {
|
||||
self.channel_paths.push(vec![channel.id]);
|
||||
}
|
||||
}
|
||||
|
||||
self.channel_paths.sort_by(|a, b| {
|
||||
let a = Self::channel_path_sorting_key(a, &self.channels_by_id);
|
||||
let b = Self::channel_path_sorting_key(b, &self.channels_by_id);
|
||||
a.cmp(b)
|
||||
});
|
||||
self.channel_paths.dedup();
|
||||
self.channel_paths.retain(|path| {
|
||||
path.iter()
|
||||
.all(|channel_id| self.channels_by_id.contains_key(channel_id))
|
||||
});
|
||||
}
|
||||
|
||||
for permission in payload.channel_permissions {
|
||||
if permission.is_admin {
|
||||
self.channels_with_admin_privileges
|
||||
.insert(permission.channel_id);
|
||||
} else {
|
||||
self.channels_with_admin_privileges
|
||||
.remove(&permission.channel_id);
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
if payload.channel_participants.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut all_user_ids = Vec::new();
|
||||
let channel_participants = payload.channel_participants;
|
||||
for entry in &channel_participants {
|
||||
for user_id in entry.participant_user_ids.iter() {
|
||||
if let Err(ix) = all_user_ids.binary_search(user_id) {
|
||||
all_user_ids.insert(ix, *user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let users = self
|
||||
.user_store
|
||||
.update(cx, |user_store, cx| user_store.get_users(all_user_ids, cx));
|
||||
Some(cx.spawn(|this, mut cx| async move {
|
||||
let users = users.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
for entry in &channel_participants {
|
||||
let mut participants: Vec<_> = entry
|
||||
.participant_user_ids
|
||||
.iter()
|
||||
.filter_map(|user_id| {
|
||||
users
|
||||
.binary_search_by_key(&user_id, |user| &user.id)
|
||||
.ok()
|
||||
.map(|ix| users[ix].clone())
|
||||
})
|
||||
.collect();
|
||||
|
||||
participants.sort_by_key(|u| u.id);
|
||||
|
||||
this.channel_participants
|
||||
.insert(entry.channel_id, participants);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
});
|
||||
anyhow::Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn channel_path_sorting_key<'a>(
|
||||
path: &'a [ChannelId],
|
||||
channels_by_id: &'a HashMap<ChannelId, Arc<Channel>>,
|
||||
) -> impl 'a + Iterator<Item = Option<&'a str>> {
|
||||
path.iter()
|
||||
.map(|id| Some(channels_by_id.get(id)?.name.as_str()))
|
||||
}
|
||||
}
|
165
crates/client/src/channel_store_tests.rs
Normal file
165
crates/client/src/channel_store_tests.rs
Normal file
|
@ -0,0 +1,165 @@
|
|||
use super::*;
|
||||
use util::http::FakeHttpClient;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_update_channels(cx: &mut AppContext) {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
|
||||
let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
|
||||
|
||||
update_channels(
|
||||
&channel_store,
|
||||
proto::UpdateChannels {
|
||||
channels: vec![
|
||||
proto::Channel {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
parent_id: None,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "a".to_string(),
|
||||
parent_id: None,
|
||||
},
|
||||
],
|
||||
channel_permissions: vec![proto::ChannelPermission {
|
||||
channel_id: 1,
|
||||
is_admin: true,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
assert_channels(
|
||||
&channel_store,
|
||||
&[
|
||||
//
|
||||
(0, "a".to_string(), false),
|
||||
(0, "b".to_string(), true),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
|
||||
update_channels(
|
||||
&channel_store,
|
||||
proto::UpdateChannels {
|
||||
channels: vec![
|
||||
proto::Channel {
|
||||
id: 3,
|
||||
name: "x".to_string(),
|
||||
parent_id: Some(1),
|
||||
},
|
||||
proto::Channel {
|
||||
id: 4,
|
||||
name: "y".to_string(),
|
||||
parent_id: Some(2),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
assert_channels(
|
||||
&channel_store,
|
||||
&[
|
||||
(0, "a".to_string(), false),
|
||||
(1, "y".to_string(), false),
|
||||
(0, "b".to_string(), true),
|
||||
(1, "x".to_string(), true),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
|
||||
let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
|
||||
|
||||
update_channels(
|
||||
&channel_store,
|
||||
proto::UpdateChannels {
|
||||
channels: vec![
|
||||
proto::Channel {
|
||||
id: 0,
|
||||
name: "a".to_string(),
|
||||
parent_id: None,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
parent_id: Some(0),
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "c".to_string(),
|
||||
parent_id: Some(1),
|
||||
},
|
||||
],
|
||||
channel_permissions: vec![proto::ChannelPermission {
|
||||
channel_id: 0,
|
||||
is_admin: true,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
// Sanity check
|
||||
assert_channels(
|
||||
&channel_store,
|
||||
&[
|
||||
//
|
||||
(0, "a".to_string(), true),
|
||||
(1, "b".to_string(), true),
|
||||
(2, "c".to_string(), true),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
|
||||
update_channels(
|
||||
&channel_store,
|
||||
proto::UpdateChannels {
|
||||
remove_channels: vec![1, 2],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
// Make sure that the 1/2/3 path is gone
|
||||
assert_channels(&channel_store, &[(0, "a".to_string(), true)], cx);
|
||||
}
|
||||
|
||||
fn update_channels(
|
||||
channel_store: &ModelHandle<ChannelStore>,
|
||||
message: proto::UpdateChannels,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let task = channel_store.update(cx, |store, cx| store.update_channels(message, cx));
|
||||
assert!(task.is_none());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_channels(
|
||||
channel_store: &ModelHandle<ChannelStore>,
|
||||
expected_channels: &[(usize, String, bool)],
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let actual = channel_store.read_with(cx, |store, _| {
|
||||
store
|
||||
.channels()
|
||||
.map(|(depth, channel)| {
|
||||
(
|
||||
depth,
|
||||
channel.name.to_string(),
|
||||
store.is_user_admin(channel.id),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(actual, expected_channels);
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
#[cfg(test)]
|
||||
mod channel_store_tests;
|
||||
|
||||
pub mod channel_store;
|
||||
pub mod telemetry;
|
||||
pub mod user;
|
||||
|
||||
|
@ -44,6 +48,7 @@ use util::channel::ReleaseChannel;
|
|||
use util::http::HttpClient;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub use channel_store::*;
|
||||
pub use rpc::*;
|
||||
pub use telemetry::ClickhouseEvent;
|
||||
pub use user::*;
|
||||
|
@ -535,6 +540,7 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn add_message_handler<M, E, H, F>(
|
||||
self: &Arc<Self>,
|
||||
model: ModelHandle<E>,
|
||||
|
@ -570,7 +576,13 @@ impl Client {
|
|||
}),
|
||||
);
|
||||
if prev_handler.is_some() {
|
||||
panic!("registered handler for the same message twice");
|
||||
let location = std::panic::Location::caller();
|
||||
panic!(
|
||||
"{}:{} registered handler for the same message {} twice",
|
||||
location.file(),
|
||||
location.line(),
|
||||
std::any::type_name::<M>()
|
||||
);
|
||||
}
|
||||
|
||||
Subscription::Message {
|
||||
|
|
|
@ -165,17 +165,29 @@ impl UserStore {
|
|||
});
|
||||
|
||||
current_user_tx.send(user).await.ok();
|
||||
|
||||
this.update(&mut cx, |_, cx| {
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
Status::SignedOut => {
|
||||
current_user_tx.send(None).await.ok();
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, _| this.clear_contacts()).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
cx.notify();
|
||||
this.clear_contacts()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Status::ConnectionLost => {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, _| this.clear_contacts()).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
cx.notify();
|
||||
this.clear_contacts()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
|||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.16.0"
|
||||
version = "0.17.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
|
|
@ -36,7 +36,8 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
|
|||
|
||||
CREATE TABLE "rooms" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"live_kit_room" VARCHAR NOT NULL
|
||||
"live_kit_room" VARCHAR NOT NULL,
|
||||
"channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "projects" (
|
||||
|
@ -184,3 +185,26 @@ CREATE UNIQUE INDEX
|
|||
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
|
||||
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
|
||||
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
|
||||
|
||||
CREATE TABLE "channels" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"name" VARCHAR NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now
|
||||
);
|
||||
|
||||
CREATE TABLE "channel_paths" (
|
||||
"id_path" TEXT NOT NULL PRIMARY KEY,
|
||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_channel_paths_on_channel_id" ON "channel_paths" ("channel_id");
|
||||
|
||||
CREATE TABLE "channel_members" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
"admin" BOOLEAN NOT NULL DEFAULT false,
|
||||
"accepted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id");
|
||||
|
|
30
crates/collab/migrations/20230727150500_add_channels.sql
Normal file
30
crates/collab/migrations/20230727150500_add_channels.sql
Normal file
|
@ -0,0 +1,30 @@
|
|||
DROP TABLE "channel_messages";
|
||||
DROP TABLE "channel_memberships";
|
||||
DROP TABLE "org_memberships";
|
||||
DROP TABLE "orgs";
|
||||
DROP TABLE "channels";
|
||||
|
||||
CREATE TABLE "channels" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"name" VARCHAR NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE "channel_paths" (
|
||||
"id_path" VARCHAR NOT NULL PRIMARY KEY,
|
||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_channel_paths_on_channel_id" ON "channel_paths" ("channel_id");
|
||||
|
||||
CREATE TABLE "channel_members" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
"admin" BOOLEAN NOT NULL DEFAULT false,
|
||||
"accepted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id");
|
||||
|
||||
ALTER TABLE rooms ADD COLUMN "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE;
|
|
@ -64,9 +64,9 @@ async fn main() {
|
|||
.expect("failed to fetch user")
|
||||
.is_none()
|
||||
{
|
||||
if let Some(email) = &github_user.email {
|
||||
if admin {
|
||||
db.create_user(
|
||||
email,
|
||||
&format!("{}@zed.dev", github_user.login),
|
||||
admin,
|
||||
db::NewUserParams {
|
||||
github_login: github_user.login,
|
||||
|
@ -76,15 +76,11 @@ async fn main() {
|
|||
)
|
||||
.await
|
||||
.expect("failed to insert user");
|
||||
} else if admin {
|
||||
db.create_user(
|
||||
&format!("{}@zed.dev", github_user.login),
|
||||
admin,
|
||||
db::NewUserParams {
|
||||
github_login: github_user.login,
|
||||
github_user_id: github_user.id,
|
||||
invite_count: 5,
|
||||
},
|
||||
} else {
|
||||
db.get_or_create_user_by_github_account(
|
||||
&github_user.login,
|
||||
Some(github_user.id),
|
||||
github_user.email.as_deref(),
|
||||
)
|
||||
.await
|
||||
.expect("failed to insert user");
|
||||
|
|
File diff suppressed because it is too large
Load diff
38
crates/collab/src/db/channel.rs
Normal file
38
crates/collab/src/db/channel.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use super::ChannelId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "channels")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_one = "super::room::Entity")]
|
||||
Room,
|
||||
#[sea_orm(has_many = "super::channel_member::Entity")]
|
||||
Member,
|
||||
}
|
||||
|
||||
impl Related<super::channel_member::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Member.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::room::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Room.def()
|
||||
}
|
||||
}
|
||||
|
||||
// impl Related<super::follower::Entity> for Entity {
|
||||
// fn to() -> RelationDef {
|
||||
// Relation::Follower.def()
|
||||
// }
|
||||
// }
|
61
crates/collab/src/db/channel_member.rs
Normal file
61
crates/collab/src/db/channel_member.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use crate::db::channel_member;
|
||||
|
||||
use super::{ChannelId, ChannelMemberId, UserId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "channel_members")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: ChannelMemberId,
|
||||
pub channel_id: ChannelId,
|
||||
pub user_id: UserId,
|
||||
pub accepted: bool,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::channel::Entity",
|
||||
from = "Column::ChannelId",
|
||||
to = "super::channel::Column::Id"
|
||||
)]
|
||||
Channel,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::channel::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Channel.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserToChannel;
|
||||
|
||||
impl Linked for UserToChannel {
|
||||
type FromEntity = super::user::Entity;
|
||||
|
||||
type ToEntity = super::channel::Entity;
|
||||
|
||||
fn link(&self) -> Vec<RelationDef> {
|
||||
vec![
|
||||
channel_member::Relation::User.def().rev(),
|
||||
channel_member::Relation::Channel.def(),
|
||||
]
|
||||
}
|
||||
}
|
15
crates/collab/src/db/channel_path.rs
Normal file
15
crates/collab/src/db/channel_path.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use super::ChannelId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "channel_paths")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id_path: String,
|
||||
pub channel_id: ChannelId,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
|
@ -1,12 +1,13 @@
|
|||
use super::RoomId;
|
||||
use super::{ChannelId, RoomId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[derive(Clone, Default, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "rooms")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: RoomId,
|
||||
pub live_kit_room: String,
|
||||
pub channel_id: Option<ChannelId>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
@ -17,6 +18,12 @@ pub enum Relation {
|
|||
Project,
|
||||
#[sea_orm(has_many = "super::follower::Entity")]
|
||||
Follower,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::channel::Entity",
|
||||
from = "Column::ChannelId",
|
||||
to = "super::channel::Column::Id"
|
||||
)]
|
||||
Channel,
|
||||
}
|
||||
|
||||
impl Related<super::room_participant::Entity> for Entity {
|
||||
|
@ -37,4 +44,10 @@ impl Related<super::follower::Entity> for Entity {
|
|||
}
|
||||
}
|
||||
|
||||
impl Related<super::channel::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Channel.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
|
|
@ -879,6 +879,453 @@ async fn test_invite_codes() {
|
|||
assert!(db.has_contact(user5, user1).await.unwrap());
|
||||
}
|
||||
|
||||
test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, {
|
||||
let a_id = db
|
||||
.create_user(
|
||||
"user1@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user1".into(),
|
||||
github_user_id: 5,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let b_id = db
|
||||
.create_user(
|
||||
"user2@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user2".into(),
|
||||
github_user_id: 6,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
|
||||
|
||||
// Make sure that people cannot read channels they haven't been invited to
|
||||
assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none());
|
||||
|
||||
db.invite_channel_member(zed_id, b_id, a_id, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.respond_to_channel_invite(zed_id, b_id, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let crdb_id = db
|
||||
.create_channel("crdb", Some(zed_id), "2", a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let livestreaming_id = db
|
||||
.create_channel("livestreaming", Some(zed_id), "3", a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let replace_id = db
|
||||
.create_channel("replace", Some(zed_id), "4", a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut members = db.get_channel_members(replace_id).await.unwrap();
|
||||
members.sort();
|
||||
assert_eq!(members, &[a_id, b_id]);
|
||||
|
||||
let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap();
|
||||
let cargo_id = db
|
||||
.create_channel("cargo", Some(rust_id), "6", a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let cargo_ra_id = db
|
||||
.create_channel("cargo-ra", Some(cargo_id), "7", a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||
assert_eq!(
|
||||
result.channels,
|
||||
vec![
|
||||
Channel {
|
||||
id: zed_id,
|
||||
name: "zed".to_string(),
|
||||
parent_id: None,
|
||||
},
|
||||
Channel {
|
||||
id: crdb_id,
|
||||
name: "crdb".to_string(),
|
||||
parent_id: Some(zed_id),
|
||||
},
|
||||
Channel {
|
||||
id: livestreaming_id,
|
||||
name: "livestreaming".to_string(),
|
||||
parent_id: Some(zed_id),
|
||||
},
|
||||
Channel {
|
||||
id: replace_id,
|
||||
name: "replace".to_string(),
|
||||
parent_id: Some(zed_id),
|
||||
},
|
||||
Channel {
|
||||
id: rust_id,
|
||||
name: "rust".to_string(),
|
||||
parent_id: None,
|
||||
},
|
||||
Channel {
|
||||
id: cargo_id,
|
||||
name: "cargo".to_string(),
|
||||
parent_id: Some(rust_id),
|
||||
},
|
||||
Channel {
|
||||
id: cargo_ra_id,
|
||||
name: "cargo-ra".to_string(),
|
||||
parent_id: Some(cargo_id),
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
let result = db.get_channels_for_user(b_id).await.unwrap();
|
||||
assert_eq!(
|
||||
result.channels,
|
||||
vec![
|
||||
Channel {
|
||||
id: zed_id,
|
||||
name: "zed".to_string(),
|
||||
parent_id: None,
|
||||
},
|
||||
Channel {
|
||||
id: crdb_id,
|
||||
name: "crdb".to_string(),
|
||||
parent_id: Some(zed_id),
|
||||
},
|
||||
Channel {
|
||||
id: livestreaming_id,
|
||||
name: "livestreaming".to_string(),
|
||||
parent_id: Some(zed_id),
|
||||
},
|
||||
Channel {
|
||||
id: replace_id,
|
||||
name: "replace".to_string(),
|
||||
parent_id: Some(zed_id),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Update member permissions
|
||||
let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await;
|
||||
assert!(set_subchannel_admin.is_err());
|
||||
let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await;
|
||||
assert!(set_channel_admin.is_ok());
|
||||
|
||||
let result = db.get_channels_for_user(b_id).await.unwrap();
|
||||
assert_eq!(
|
||||
result.channels,
|
||||
vec![
|
||||
Channel {
|
||||
id: zed_id,
|
||||
name: "zed".to_string(),
|
||||
parent_id: None,
|
||||
},
|
||||
Channel {
|
||||
id: crdb_id,
|
||||
name: "crdb".to_string(),
|
||||
parent_id: Some(zed_id),
|
||||
},
|
||||
Channel {
|
||||
id: livestreaming_id,
|
||||
name: "livestreaming".to_string(),
|
||||
parent_id: Some(zed_id),
|
||||
},
|
||||
Channel {
|
||||
id: replace_id,
|
||||
name: "replace".to_string(),
|
||||
parent_id: Some(zed_id),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Remove a single channel
|
||||
db.remove_channel(crdb_id, a_id).await.unwrap();
|
||||
assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none());
|
||||
|
||||
// Remove a channel tree
|
||||
let (mut channel_ids, user_ids) = db.remove_channel(rust_id, a_id).await.unwrap();
|
||||
channel_ids.sort();
|
||||
assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]);
|
||||
assert_eq!(user_ids, &[a_id]);
|
||||
|
||||
assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none());
|
||||
});
|
||||
|
||||
test_both_dbs!(
|
||||
test_joining_channels_postgres,
|
||||
test_joining_channels_sqlite,
|
||||
db,
|
||||
{
|
||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||
|
||||
let user_1 = db
|
||||
.create_user(
|
||||
"user1@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user1".into(),
|
||||
github_user_id: 5,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let user_2 = db
|
||||
.create_user(
|
||||
"user2@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user2".into(),
|
||||
github_user_id: 6,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let channel_1 = db
|
||||
.create_root_channel("channel_1", "1", user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
|
||||
|
||||
// can join a room with membership to its channel
|
||||
let joined_room = db
|
||||
.join_room(room_1, user_1, ConnectionId { owner_id, id: 1 })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(joined_room.room.participants.len(), 1);
|
||||
|
||||
drop(joined_room);
|
||||
// cannot join a room without membership to its channel
|
||||
assert!(db
|
||||
.join_room(room_1, user_2, ConnectionId { owner_id, id: 1 })
|
||||
.await
|
||||
.is_err());
|
||||
}
|
||||
);
|
||||
|
||||
test_both_dbs!(
|
||||
test_channel_invites_postgres,
|
||||
test_channel_invites_sqlite,
|
||||
db,
|
||||
{
|
||||
db.create_server("test").await.unwrap();
|
||||
|
||||
let user_1 = db
|
||||
.create_user(
|
||||
"user1@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user1".into(),
|
||||
github_user_id: 5,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let user_2 = db
|
||||
.create_user(
|
||||
"user2@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user2".into(),
|
||||
github_user_id: 6,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let user_3 = db
|
||||
.create_user(
|
||||
"user3@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user3".into(),
|
||||
github_user_id: 7,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let channel_1_1 = db
|
||||
.create_root_channel("channel_1", "1", user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let channel_1_2 = db
|
||||
.create_root_channel("channel_2", "2", user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_1_1, user_2, user_1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(channel_1_2, user_2, user_1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(channel_1_1, user_3, user_1, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user_2_invites = db
|
||||
.get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2]
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|channel| channel.id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]);
|
||||
|
||||
let user_3_invites = db
|
||||
.get_channel_invites_for_user(user_3) // -> [channel_1_1]
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|channel| channel.id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(user_3_invites, &[channel_1_1]);
|
||||
|
||||
let members = db
|
||||
.get_channel_member_details(channel_1_1, user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: user_1.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
admin: true,
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_2.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
admin: false,
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_3.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
admin: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
db.respond_to_channel_invite(channel_1_1, user_2, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let channel_1_3 = db
|
||||
.create_channel("channel_3", Some(channel_1_1), "1", user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let members = db
|
||||
.get_channel_member_details(channel_1_3, user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: user_1.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
admin: true,
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_2.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
admin: false,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
test_both_dbs!(
|
||||
test_channel_renames_postgres,
|
||||
test_channel_renames_sqlite,
|
||||
db,
|
||||
{
|
||||
db.create_server("test").await.unwrap();
|
||||
|
||||
let user_1 = db
|
||||
.create_user(
|
||||
"user1@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user1".into(),
|
||||
github_user_id: 5,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let user_2 = db
|
||||
.create_user(
|
||||
"user2@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user2".into(),
|
||||
github_user_id: 6,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
|
||||
|
||||
db.rename_channel(zed_id, user_1, "#zed-archive")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let zed_archive_id = zed_id;
|
||||
|
||||
let (channel, _) = db
|
||||
.get_channel(zed_archive_id, user_1)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(channel.name, "zed-archive");
|
||||
|
||||
let non_permissioned_rename = db
|
||||
.rename_channel(zed_archive_id, user_2, "hacked-lol")
|
||||
.await;
|
||||
assert!(non_permissioned_rename.is_err());
|
||||
|
||||
let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await;
|
||||
assert!(bad_name_rename.is_err())
|
||||
}
|
||||
);
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiple_signup_overwrite() {
|
||||
let test_db = TestDb::postgres(build_background_executor());
|
||||
|
|
|
@ -26,6 +26,8 @@ pub enum Relation {
|
|||
RoomParticipant,
|
||||
#[sea_orm(has_many = "super::project::Entity")]
|
||||
HostedProjects,
|
||||
#[sea_orm(has_many = "super::channel_member::Entity")]
|
||||
ChannelMemberships,
|
||||
}
|
||||
|
||||
impl Related<super::access_token::Entity> for Entity {
|
||||
|
@ -46,4 +48,10 @@ impl Related<super::project::Entity> for Entity {
|
|||
}
|
||||
}
|
||||
|
||||
impl Related<super::channel_member::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ChannelMemberships.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
|
|
@ -2,7 +2,7 @@ mod connection_pool;
|
|||
|
||||
use crate::{
|
||||
auth,
|
||||
db::{self, Database, ProjectId, RoomId, ServerId, User, UserId},
|
||||
db::{self, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, UserId},
|
||||
executor::Executor,
|
||||
AppState, Result,
|
||||
};
|
||||
|
@ -34,7 +34,10 @@ use futures::{
|
|||
use lazy_static::lazy_static;
|
||||
use prometheus::{register_int_gauge, IntGauge};
|
||||
use rpc::{
|
||||
proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage},
|
||||
proto::{
|
||||
self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
|
||||
RequestMessage,
|
||||
},
|
||||
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||
};
|
||||
use serde::{Serialize, Serializer};
|
||||
|
@ -239,6 +242,15 @@ impl Server {
|
|||
.add_request_handler(request_contact)
|
||||
.add_request_handler(remove_contact)
|
||||
.add_request_handler(respond_to_contact_request)
|
||||
.add_request_handler(create_channel)
|
||||
.add_request_handler(remove_channel)
|
||||
.add_request_handler(invite_channel_member)
|
||||
.add_request_handler(remove_channel_member)
|
||||
.add_request_handler(set_channel_member_admin)
|
||||
.add_request_handler(rename_channel)
|
||||
.add_request_handler(get_channel_members)
|
||||
.add_request_handler(respond_to_channel_invite)
|
||||
.add_request_handler(join_channel)
|
||||
.add_request_handler(follow)
|
||||
.add_message_handler(unfollow)
|
||||
.add_message_handler(update_followers)
|
||||
|
@ -287,6 +299,15 @@ impl Server {
|
|||
"refreshed room"
|
||||
);
|
||||
room_updated(&refreshed_room.room, &peer);
|
||||
if let Some(channel_id) = refreshed_room.channel_id {
|
||||
channel_updated(
|
||||
channel_id,
|
||||
&refreshed_room.room,
|
||||
&refreshed_room.channel_members,
|
||||
&peer,
|
||||
&*pool.lock(),
|
||||
);
|
||||
}
|
||||
contacts_to_update
|
||||
.extend(refreshed_room.stale_participant_user_ids.iter().copied());
|
||||
contacts_to_update
|
||||
|
@ -508,15 +529,21 @@ impl Server {
|
|||
this.app_state.db.set_user_connected_once(user_id, true).await?;
|
||||
}
|
||||
|
||||
let (contacts, invite_code) = future::try_join(
|
||||
let (contacts, invite_code, channels_for_user, channel_invites) = future::try_join4(
|
||||
this.app_state.db.get_contacts(user_id),
|
||||
this.app_state.db.get_invite_code_for_user(user_id)
|
||||
this.app_state.db.get_invite_code_for_user(user_id),
|
||||
this.app_state.db.get_channels_for_user(user_id),
|
||||
this.app_state.db.get_channel_invites_for_user(user_id)
|
||||
).await?;
|
||||
|
||||
{
|
||||
let mut pool = this.connection_pool.lock();
|
||||
pool.add_connection(connection_id, user_id, user.admin);
|
||||
this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
|
||||
this.peer.send(connection_id, build_initial_channels_update(
|
||||
channels_for_user,
|
||||
channel_invites
|
||||
))?;
|
||||
|
||||
if let Some((code, count)) = invite_code {
|
||||
this.peer.send(connection_id, proto::UpdateInviteInfo {
|
||||
|
@ -857,42 +884,41 @@ async fn create_room(
|
|||
session: Session,
|
||||
) -> Result<()> {
|
||||
let live_kit_room = nanoid::nanoid!(30);
|
||||
let live_kit_connection_info = if let Some(live_kit) = session.live_kit_client.as_ref() {
|
||||
if let Some(_) = live_kit
|
||||
.create_room(live_kit_room.clone())
|
||||
.await
|
||||
.trace_err()
|
||||
{
|
||||
if let Some(token) = live_kit
|
||||
|
||||
let live_kit_connection_info = {
|
||||
let live_kit_room = live_kit_room.clone();
|
||||
let live_kit = session.live_kit_client.as_ref();
|
||||
|
||||
util::async_iife!({
|
||||
let live_kit = live_kit?;
|
||||
|
||||
live_kit
|
||||
.create_room(live_kit_room.clone())
|
||||
.await
|
||||
.trace_err()?;
|
||||
|
||||
let token = live_kit
|
||||
.room_token(&live_kit_room, &session.user_id.to_string())
|
||||
.trace_err()
|
||||
{
|
||||
Some(proto::LiveKitConnectionInfo {
|
||||
server_url: live_kit.url().into(),
|
||||
token,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
.trace_err()?;
|
||||
|
||||
{
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.create_room(session.user_id, session.connection_id, &live_kit_room)
|
||||
.await?;
|
||||
|
||||
response.send(proto::CreateRoomResponse {
|
||||
room: Some(room.clone()),
|
||||
live_kit_connection_info,
|
||||
})?;
|
||||
Some(proto::LiveKitConnectionInfo {
|
||||
server_url: live_kit.url().into(),
|
||||
token,
|
||||
})
|
||||
})
|
||||
}
|
||||
.await;
|
||||
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.create_room(session.user_id, session.connection_id, &live_kit_room)
|
||||
.await?;
|
||||
|
||||
response.send(proto::CreateRoomResponse {
|
||||
room: Some(room.clone()),
|
||||
live_kit_connection_info,
|
||||
})?;
|
||||
|
||||
update_user_contacts(session.user_id, &session).await?;
|
||||
Ok(())
|
||||
|
@ -904,16 +930,26 @@ async fn join_room(
|
|||
session: Session,
|
||||
) -> Result<()> {
|
||||
let room_id = RoomId::from_proto(request.id);
|
||||
let room = {
|
||||
let joined_room = {
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.join_room(room_id, session.user_id, session.connection_id)
|
||||
.await?;
|
||||
room_updated(&room, &session.peer);
|
||||
room.clone()
|
||||
room_updated(&room.room, &session.peer);
|
||||
room.into_inner()
|
||||
};
|
||||
|
||||
if let Some(channel_id) = joined_room.channel_id {
|
||||
channel_updated(
|
||||
channel_id,
|
||||
&joined_room.room,
|
||||
&joined_room.channel_members,
|
||||
&session.peer,
|
||||
&*session.connection_pool().await,
|
||||
)
|
||||
}
|
||||
|
||||
for connection_id in session
|
||||
.connection_pool()
|
||||
.await
|
||||
|
@ -932,7 +968,10 @@ async fn join_room(
|
|||
|
||||
let live_kit_connection_info = if let Some(live_kit) = session.live_kit_client.as_ref() {
|
||||
if let Some(token) = live_kit
|
||||
.room_token(&room.live_kit_room, &session.user_id.to_string())
|
||||
.room_token(
|
||||
&joined_room.room.live_kit_room,
|
||||
&session.user_id.to_string(),
|
||||
)
|
||||
.trace_err()
|
||||
{
|
||||
Some(proto::LiveKitConnectionInfo {
|
||||
|
@ -947,7 +986,8 @@ async fn join_room(
|
|||
};
|
||||
|
||||
response.send(proto::JoinRoomResponse {
|
||||
room: Some(room),
|
||||
room: Some(joined_room.room),
|
||||
channel_id: joined_room.channel_id.map(|id| id.to_proto()),
|
||||
live_kit_connection_info,
|
||||
})?;
|
||||
|
||||
|
@ -960,6 +1000,9 @@ async fn rejoin_room(
|
|||
response: Response<proto::RejoinRoom>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let room;
|
||||
let channel_id;
|
||||
let channel_members;
|
||||
{
|
||||
let mut rejoined_room = session
|
||||
.db()
|
||||
|
@ -1121,6 +1164,22 @@ async fn rejoin_room(
|
|||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
let rejoined_room = rejoined_room.into_inner();
|
||||
|
||||
room = rejoined_room.room;
|
||||
channel_id = rejoined_room.channel_id;
|
||||
channel_members = rejoined_room.channel_members;
|
||||
}
|
||||
|
||||
if let Some(channel_id) = channel_id {
|
||||
channel_updated(
|
||||
channel_id,
|
||||
&room,
|
||||
&channel_members,
|
||||
&session.peer,
|
||||
&*session.connection_pool().await,
|
||||
);
|
||||
}
|
||||
|
||||
update_user_contacts(session.user_id, &session).await?;
|
||||
|
@ -1282,11 +1341,12 @@ async fn update_participant_location(
|
|||
let location = request
|
||||
.location
|
||||
.ok_or_else(|| anyhow!("invalid location"))?;
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
|
||||
let db = session.db().await;
|
||||
let room = db
|
||||
.update_room_participant_location(room_id, session.connection_id, location)
|
||||
.await?;
|
||||
|
||||
room_updated(&room, &session.peer);
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
|
@ -2084,6 +2144,340 @@ async fn remove_contact(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_channel(
|
||||
request: proto::CreateChannel,
|
||||
response: Response<proto::CreateChannel>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
|
||||
|
||||
if let Some(live_kit) = session.live_kit_client.as_ref() {
|
||||
live_kit.create_room(live_kit_room.clone()).await?;
|
||||
}
|
||||
|
||||
let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
|
||||
let id = db
|
||||
.create_channel(&request.name, parent_id, &live_kit_room, session.user_id)
|
||||
.await?;
|
||||
|
||||
let channel = proto::Channel {
|
||||
id: id.to_proto(),
|
||||
name: request.name,
|
||||
parent_id: request.parent_id,
|
||||
};
|
||||
|
||||
response.send(proto::ChannelResponse {
|
||||
channel: Some(channel.clone()),
|
||||
})?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update.channels.push(channel);
|
||||
|
||||
let user_ids_to_notify = if let Some(parent_id) = parent_id {
|
||||
db.get_channel_members(parent_id).await?
|
||||
} else {
|
||||
vec![session.user_id]
|
||||
};
|
||||
|
||||
let connection_pool = session.connection_pool().await;
|
||||
for user_id in user_ids_to_notify {
|
||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||
let mut update = update.clone();
|
||||
if user_id == session.user_id {
|
||||
update.channel_permissions.push(proto::ChannelPermission {
|
||||
channel_id: id.to_proto(),
|
||||
is_admin: true,
|
||||
});
|
||||
}
|
||||
session.peer.send(connection_id, update)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_channel(
|
||||
request: proto::RemoveChannel,
|
||||
response: Response<proto::RemoveChannel>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
|
||||
let channel_id = request.channel_id;
|
||||
let (removed_channels, member_ids) = db
|
||||
.remove_channel(ChannelId::from_proto(channel_id), session.user_id)
|
||||
.await?;
|
||||
response.send(proto::Ack {})?;
|
||||
|
||||
// Notify members of removed channels
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update
|
||||
.remove_channels
|
||||
.extend(removed_channels.into_iter().map(|id| id.to_proto()));
|
||||
|
||||
let connection_pool = session.connection_pool().await;
|
||||
for member_id in member_ids {
|
||||
for connection_id in connection_pool.user_connection_ids(member_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn invite_channel_member(
|
||||
request: proto::InviteChannelMember,
|
||||
response: Response<proto::InviteChannelMember>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let invitee_id = UserId::from_proto(request.user_id);
|
||||
db.invite_channel_member(channel_id, invitee_id, session.user_id, request.admin)
|
||||
.await?;
|
||||
|
||||
let (channel, _) = db
|
||||
.get_channel(channel_id, session.user_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("channel not found"))?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update.channel_invitations.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
parent_id: None,
|
||||
});
|
||||
for connection_id in session
|
||||
.connection_pool()
|
||||
.await
|
||||
.user_connection_ids(invitee_id)
|
||||
{
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_channel_member(
|
||||
request: proto::RemoveChannelMember,
|
||||
response: Response<proto::RemoveChannelMember>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let member_id = UserId::from_proto(request.user_id);
|
||||
|
||||
db.remove_channel_member(channel_id, member_id, session.user_id)
|
||||
.await?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update.remove_channels.push(channel_id.to_proto());
|
||||
|
||||
for connection_id in session
|
||||
.connection_pool()
|
||||
.await
|
||||
.user_connection_ids(member_id)
|
||||
{
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_channel_member_admin(
|
||||
request: proto::SetChannelMemberAdmin,
|
||||
response: Response<proto::SetChannelMemberAdmin>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let member_id = UserId::from_proto(request.user_id);
|
||||
db.set_channel_member_admin(channel_id, session.user_id, member_id, request.admin)
|
||||
.await?;
|
||||
|
||||
let (channel, has_accepted) = db
|
||||
.get_channel(channel_id, member_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("channel not found"))?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
if has_accepted {
|
||||
update.channel_permissions.push(proto::ChannelPermission {
|
||||
channel_id: channel.id.to_proto(),
|
||||
is_admin: request.admin,
|
||||
});
|
||||
}
|
||||
|
||||
for connection_id in session
|
||||
.connection_pool()
|
||||
.await
|
||||
.user_connection_ids(member_id)
|
||||
{
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rename_channel(
|
||||
request: proto::RenameChannel,
|
||||
response: Response<proto::RenameChannel>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let new_name = db
|
||||
.rename_channel(channel_id, session.user_id, &request.name)
|
||||
.await?;
|
||||
|
||||
let channel = proto::Channel {
|
||||
id: request.channel_id,
|
||||
name: new_name,
|
||||
parent_id: None,
|
||||
};
|
||||
response.send(proto::ChannelResponse {
|
||||
channel: Some(channel.clone()),
|
||||
})?;
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update.channels.push(channel);
|
||||
|
||||
let member_ids = db.get_channel_members(channel_id).await?;
|
||||
|
||||
let connection_pool = session.connection_pool().await;
|
||||
for member_id in member_ids {
|
||||
for connection_id in connection_pool.user_connection_ids(member_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_channel_members(
|
||||
request: proto::GetChannelMembers,
|
||||
response: Response<proto::GetChannelMembers>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let members = db
|
||||
.get_channel_member_details(channel_id, session.user_id)
|
||||
.await?;
|
||||
response.send(proto::GetChannelMembersResponse { members })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn respond_to_channel_invite(
|
||||
request: proto::RespondToChannelInvite,
|
||||
response: Response<proto::RespondToChannelInvite>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
db.respond_to_channel_invite(channel_id, session.user_id, request.accept)
|
||||
.await?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update
|
||||
.remove_channel_invitations
|
||||
.push(channel_id.to_proto());
|
||||
if request.accept {
|
||||
let result = db.get_channels_for_user(session.user_id).await?;
|
||||
update
|
||||
.channels
|
||||
.extend(result.channels.into_iter().map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
parent_id: channel.parent_id.map(ChannelId::to_proto),
|
||||
}));
|
||||
update
|
||||
.channel_participants
|
||||
.extend(
|
||||
result
|
||||
.channel_participants
|
||||
.into_iter()
|
||||
.map(|(channel_id, user_ids)| proto::ChannelParticipants {
|
||||
channel_id: channel_id.to_proto(),
|
||||
participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
|
||||
}),
|
||||
);
|
||||
update
|
||||
.channel_permissions
|
||||
.extend(
|
||||
result
|
||||
.channels_with_admin_privileges
|
||||
.into_iter()
|
||||
.map(|channel_id| proto::ChannelPermission {
|
||||
channel_id: channel_id.to_proto(),
|
||||
is_admin: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
session.peer.send(session.connection_id, update)?;
|
||||
response.send(proto::Ack {})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn join_channel(
|
||||
request: proto::JoinChannel,
|
||||
response: Response<proto::JoinChannel>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
|
||||
let joined_room = {
|
||||
leave_room_for_session(&session).await?;
|
||||
let db = session.db().await;
|
||||
|
||||
let room_id = db.room_id_for_channel(channel_id).await?;
|
||||
|
||||
let joined_room = db
|
||||
.join_room(room_id, session.user_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| {
|
||||
let token = live_kit
|
||||
.room_token(
|
||||
&joined_room.room.live_kit_room,
|
||||
&session.user_id.to_string(),
|
||||
)
|
||||
.trace_err()?;
|
||||
|
||||
Some(LiveKitConnectionInfo {
|
||||
server_url: live_kit.url().into(),
|
||||
token,
|
||||
})
|
||||
});
|
||||
|
||||
response.send(proto::JoinRoomResponse {
|
||||
room: Some(joined_room.room.clone()),
|
||||
channel_id: joined_room.channel_id.map(|id| id.to_proto()),
|
||||
live_kit_connection_info,
|
||||
})?;
|
||||
|
||||
room_updated(&joined_room.room, &session.peer);
|
||||
|
||||
joined_room.into_inner()
|
||||
};
|
||||
|
||||
channel_updated(
|
||||
channel_id,
|
||||
&joined_room.room,
|
||||
&joined_room.channel_members,
|
||||
&session.peer,
|
||||
&*session.connection_pool().await,
|
||||
);
|
||||
|
||||
update_user_contacts(session.user_id, &session).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
let project_connection_ids = session
|
||||
|
@ -2154,6 +2548,52 @@ fn to_tungstenite_message(message: AxumMessage) -> TungsteniteMessage {
|
|||
}
|
||||
}
|
||||
|
||||
fn build_initial_channels_update(
|
||||
channels: ChannelsForUser,
|
||||
channel_invites: Vec<db::Channel>,
|
||||
) -> proto::UpdateChannels {
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
|
||||
for channel in channels.channels {
|
||||
update.channels.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
parent_id: channel.parent_id.map(|id| id.to_proto()),
|
||||
});
|
||||
}
|
||||
|
||||
for (channel_id, participants) in channels.channel_participants {
|
||||
update
|
||||
.channel_participants
|
||||
.push(proto::ChannelParticipants {
|
||||
channel_id: channel_id.to_proto(),
|
||||
participant_user_ids: participants.into_iter().map(|id| id.to_proto()).collect(),
|
||||
});
|
||||
}
|
||||
|
||||
update
|
||||
.channel_permissions
|
||||
.extend(
|
||||
channels
|
||||
.channels_with_admin_privileges
|
||||
.into_iter()
|
||||
.map(|id| proto::ChannelPermission {
|
||||
channel_id: id.to_proto(),
|
||||
is_admin: true,
|
||||
}),
|
||||
);
|
||||
|
||||
for channel in channel_invites {
|
||||
update.channel_invitations.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
parent_id: None,
|
||||
});
|
||||
}
|
||||
|
||||
update
|
||||
}
|
||||
|
||||
fn build_initial_contacts_update(
|
||||
contacts: Vec<db::Contact>,
|
||||
pool: &ConnectionPool,
|
||||
|
@ -2218,8 +2658,42 @@ fn room_updated(room: &proto::Room, peer: &Peer) {
|
|||
);
|
||||
}
|
||||
|
||||
fn channel_updated(
|
||||
channel_id: ChannelId,
|
||||
room: &proto::Room,
|
||||
channel_members: &[UserId],
|
||||
peer: &Peer,
|
||||
pool: &ConnectionPool,
|
||||
) {
|
||||
let participants = room
|
||||
.participants
|
||||
.iter()
|
||||
.map(|p| p.user_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
broadcast(
|
||||
None,
|
||||
channel_members
|
||||
.iter()
|
||||
.flat_map(|user_id| pool.user_connection_ids(*user_id)),
|
||||
|peer_id| {
|
||||
peer.send(
|
||||
peer_id.into(),
|
||||
proto::UpdateChannels {
|
||||
channel_participants: vec![proto::ChannelParticipants {
|
||||
channel_id: channel_id.to_proto(),
|
||||
participant_user_ids: participants.clone(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
|
||||
let contacts = db.get_contacts(user_id).await?;
|
||||
let busy = db.is_user_busy(user_id).await?;
|
||||
|
||||
|
@ -2259,6 +2733,10 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
|
|||
let canceled_calls_to_user_ids;
|
||||
let live_kit_room;
|
||||
let delete_live_kit_room;
|
||||
let room;
|
||||
let channel_members;
|
||||
let channel_id;
|
||||
|
||||
if let Some(mut left_room) = session.db().await.leave_room(session.connection_id).await? {
|
||||
contacts_to_update.insert(session.user_id);
|
||||
|
||||
|
@ -2266,15 +2744,29 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
|
|||
project_left(project, session);
|
||||
}
|
||||
|
||||
room_updated(&left_room.room, &session.peer);
|
||||
room_id = RoomId::from_proto(left_room.room.id);
|
||||
canceled_calls_to_user_ids = mem::take(&mut left_room.canceled_calls_to_user_ids);
|
||||
live_kit_room = mem::take(&mut left_room.room.live_kit_room);
|
||||
delete_live_kit_room = left_room.room.participants.is_empty();
|
||||
delete_live_kit_room = left_room.deleted;
|
||||
room = mem::take(&mut left_room.room);
|
||||
channel_members = mem::take(&mut left_room.channel_members);
|
||||
channel_id = left_room.channel_id;
|
||||
|
||||
room_updated(&room, &session.peer);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(channel_id) = channel_id {
|
||||
channel_updated(
|
||||
channel_id,
|
||||
&room,
|
||||
&channel_members,
|
||||
&session.peer,
|
||||
&*session.connection_pool().await,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let pool = session.connection_pool().await;
|
||||
for canceled_user_id in canceled_calls_to_user_ids {
|
||||
|
|
|
@ -5,14 +5,15 @@ use crate::{
|
|||
AppState,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use call::ActiveCall;
|
||||
use call::{ActiveCall, Room};
|
||||
use client::{
|
||||
self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
|
||||
self, proto::PeerId, ChannelStore, Client, Connection, Credentials, EstablishConnectionError,
|
||||
UserStore,
|
||||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, WindowHandle};
|
||||
use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
|
||||
use language::LanguageRegistry;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
|
@ -30,6 +31,7 @@ use std::{
|
|||
use util::http::FakeHttpClient;
|
||||
use workspace::Workspace;
|
||||
|
||||
mod channel_tests;
|
||||
mod integration_tests;
|
||||
mod randomized_integration_tests;
|
||||
|
||||
|
@ -98,6 +100,9 @@ impl TestServer {
|
|||
|
||||
async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
|
||||
cx.update(|cx| {
|
||||
if cx.has_global::<SettingsStore>() {
|
||||
panic!("Same cx used to create two test clients")
|
||||
}
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
});
|
||||
|
||||
|
@ -183,13 +188,16 @@ impl TestServer {
|
|||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
let channel_store =
|
||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||
let app_state = Arc::new(workspace::AppState {
|
||||
client: client.clone(),
|
||||
user_store: user_store.clone(),
|
||||
channel_store: channel_store.clone(),
|
||||
languages: Arc::new(LanguageRegistry::test()),
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
initialize_workspace: |_, _, _, _| unimplemented!(),
|
||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||
background_actions: || &[],
|
||||
});
|
||||
|
||||
|
@ -210,12 +218,9 @@ impl TestServer {
|
|||
.unwrap();
|
||||
|
||||
let client = TestClient {
|
||||
client,
|
||||
app_state,
|
||||
username: name.to_string(),
|
||||
state: Default::default(),
|
||||
user_store,
|
||||
fs,
|
||||
language_registry: Arc::new(LanguageRegistry::test()),
|
||||
};
|
||||
client.wait_for_current_user(cx).await;
|
||||
client
|
||||
|
@ -243,6 +248,7 @@ impl TestServer {
|
|||
let (client_a, cx_a) = left.last_mut().unwrap();
|
||||
for (client_b, cx_b) in right {
|
||||
client_a
|
||||
.app_state
|
||||
.user_store
|
||||
.update(*cx_a, |store, cx| {
|
||||
store.request_contact(client_b.user_id().unwrap(), cx)
|
||||
|
@ -251,6 +257,7 @@ impl TestServer {
|
|||
.unwrap();
|
||||
cx_a.foreground().run_until_parked();
|
||||
client_b
|
||||
.app_state
|
||||
.user_store
|
||||
.update(*cx_b, |store, cx| {
|
||||
store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
|
||||
|
@ -261,6 +268,52 @@ impl TestServer {
|
|||
}
|
||||
}
|
||||
|
||||
async fn make_channel(
|
||||
&self,
|
||||
channel: &str,
|
||||
admin: (&TestClient, &mut TestAppContext),
|
||||
members: &mut [(&TestClient, &mut TestAppContext)],
|
||||
) -> u64 {
|
||||
let (admin_client, admin_cx) = admin;
|
||||
let channel_id = admin_client
|
||||
.app_state
|
||||
.channel_store
|
||||
.update(admin_cx, |channel_store, cx| {
|
||||
channel_store.create_channel(channel, None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for (member_client, member_cx) in members {
|
||||
admin_client
|
||||
.app_state
|
||||
.channel_store
|
||||
.update(admin_cx, |channel_store, cx| {
|
||||
channel_store.invite_member(
|
||||
channel_id,
|
||||
member_client.user_id().unwrap(),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
admin_cx.foreground().run_until_parked();
|
||||
|
||||
member_client
|
||||
.app_state
|
||||
.channel_store
|
||||
.update(*member_cx, |channels, _| {
|
||||
channels.respond_to_channel_invite(channel_id, true)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
channel_id
|
||||
}
|
||||
|
||||
async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
|
||||
self.make_contacts(clients).await;
|
||||
|
||||
|
@ -312,12 +365,9 @@ impl Drop for TestServer {
|
|||
}
|
||||
|
||||
struct TestClient {
|
||||
client: Arc<Client>,
|
||||
username: String,
|
||||
state: RefCell<TestClientState>,
|
||||
pub user_store: ModelHandle<UserStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<FakeFs>,
|
||||
app_state: Arc<workspace::AppState>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -331,7 +381,7 @@ impl Deref for TestClient {
|
|||
type Target = Arc<Client>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.client
|
||||
&self.app_state.client
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -342,22 +392,45 @@ struct ContactsSummary {
|
|||
}
|
||||
|
||||
impl TestClient {
|
||||
pub fn fs(&self) -> &FakeFs {
|
||||
self.app_state.fs.as_fake()
|
||||
}
|
||||
|
||||
pub fn channel_store(&self) -> &ModelHandle<ChannelStore> {
|
||||
&self.app_state.channel_store
|
||||
}
|
||||
|
||||
pub fn user_store(&self) -> &ModelHandle<UserStore> {
|
||||
&self.app_state.user_store
|
||||
}
|
||||
|
||||
pub fn language_registry(&self) -> &Arc<LanguageRegistry> {
|
||||
&self.app_state.languages
|
||||
}
|
||||
|
||||
pub fn client(&self) -> &Arc<Client> {
|
||||
&self.app_state.client
|
||||
}
|
||||
|
||||
pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
|
||||
UserId::from_proto(
|
||||
self.user_store
|
||||
self.app_state
|
||||
.user_store
|
||||
.read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
|
||||
)
|
||||
}
|
||||
|
||||
async fn wait_for_current_user(&self, cx: &TestAppContext) {
|
||||
let mut authed_user = self
|
||||
.app_state
|
||||
.user_store
|
||||
.read_with(cx, |user_store, _| user_store.watch_current_user());
|
||||
while authed_user.next().await.unwrap().is_none() {}
|
||||
}
|
||||
|
||||
async fn clear_contacts(&self, cx: &mut TestAppContext) {
|
||||
self.user_store
|
||||
self.app_state
|
||||
.user_store
|
||||
.update(cx, |store, _| store.clear_contacts())
|
||||
.await;
|
||||
}
|
||||
|
@ -395,23 +468,25 @@ impl TestClient {
|
|||
}
|
||||
|
||||
fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
|
||||
self.user_store.read_with(cx, |store, _| ContactsSummary {
|
||||
current: store
|
||||
.contacts()
|
||||
.iter()
|
||||
.map(|contact| contact.user.github_login.clone())
|
||||
.collect(),
|
||||
outgoing_requests: store
|
||||
.outgoing_contact_requests()
|
||||
.iter()
|
||||
.map(|user| user.github_login.clone())
|
||||
.collect(),
|
||||
incoming_requests: store
|
||||
.incoming_contact_requests()
|
||||
.iter()
|
||||
.map(|user| user.github_login.clone())
|
||||
.collect(),
|
||||
})
|
||||
self.app_state
|
||||
.user_store
|
||||
.read_with(cx, |store, _| ContactsSummary {
|
||||
current: store
|
||||
.contacts()
|
||||
.iter()
|
||||
.map(|contact| contact.user.github_login.clone())
|
||||
.collect(),
|
||||
outgoing_requests: store
|
||||
.outgoing_contact_requests()
|
||||
.iter()
|
||||
.map(|user| user.github_login.clone())
|
||||
.collect(),
|
||||
incoming_requests: store
|
||||
.incoming_contact_requests()
|
||||
.iter()
|
||||
.map(|user| user.github_login.clone())
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn build_local_project(
|
||||
|
@ -421,10 +496,10 @@ impl TestClient {
|
|||
) -> (ModelHandle<Project>, WorktreeId) {
|
||||
let project = cx.update(|cx| {
|
||||
Project::local(
|
||||
self.client.clone(),
|
||||
self.user_store.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.fs.clone(),
|
||||
self.client().clone(),
|
||||
self.app_state.user_store.clone(),
|
||||
self.app_state.languages.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
@ -450,8 +525,8 @@ impl TestClient {
|
|||
room.update(guest_cx, |room, cx| {
|
||||
room.join_project(
|
||||
host_project_id,
|
||||
self.language_registry.clone(),
|
||||
self.fs.clone(),
|
||||
self.app_state.languages.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
@ -464,12 +539,36 @@ impl TestClient {
|
|||
project: &ModelHandle<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> WindowHandle<Workspace> {
|
||||
cx.add_window(|cx| Workspace::test_new(project.clone(), cx))
|
||||
cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestClient {
|
||||
fn drop(&mut self) {
|
||||
self.client.teardown();
|
||||
self.app_state.client.teardown();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
struct RoomParticipants {
|
||||
remote: Vec<String>,
|
||||
pending: Vec<String>,
|
||||
}
|
||||
|
||||
fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
|
||||
room.read_with(cx, |room, _| {
|
||||
let mut remote = room
|
||||
.remote_participants()
|
||||
.iter()
|
||||
.map(|(_, participant)| participant.user.github_login.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let mut pending = room
|
||||
.pending_participants()
|
||||
.iter()
|
||||
.map(|user| user.github_login.clone())
|
||||
.collect::<Vec<_>>();
|
||||
remote.sort();
|
||||
pending.sort();
|
||||
RoomParticipants { remote, pending }
|
||||
})
|
||||
}
|
||||
|
|
922
crates/collab/src/tests/channel_tests.rs
Normal file
922
crates/collab/src/tests/channel_tests.rs
Normal file
|
@ -0,0 +1,922 @@
|
|||
use crate::{
|
||||
rpc::RECONNECT_TIMEOUT,
|
||||
tests::{room_participants, RoomParticipants, TestServer},
|
||||
};
|
||||
use call::ActiveCall;
|
||||
use client::{ChannelId, ChannelMembership, ChannelStore, User};
|
||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
|
||||
use rpc::{proto, RECEIVE_TIMEOUT};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_core_channels(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let channel_a_id = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.create_channel("channel-a", None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_b_id = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.create_channel("channel-b", Some(channel_a_id), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
assert_channels(
|
||||
client_a.channel_store(),
|
||||
cx_a,
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
depth: 0,
|
||||
user_is_admin: true,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
depth: 1,
|
||||
user_is_admin: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
client_b.channel_store().read_with(cx_b, |channels, _| {
|
||||
assert!(channels.channels().collect::<Vec<_>>().is_empty())
|
||||
});
|
||||
|
||||
// Invite client B to channel A as client A.
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| {
|
||||
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||
|
||||
let invite = store.invite_member(channel_a_id, client_b.user_id().unwrap(), false, cx);
|
||||
|
||||
// Make sure we're synchronously storing the pending invite
|
||||
assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||
invite
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A sees that B has been invited.
|
||||
deterministic.run_until_parked();
|
||||
assert_channel_invitations(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
depth: 0,
|
||||
user_is_admin: false,
|
||||
}],
|
||||
);
|
||||
|
||||
let members = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| {
|
||||
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||
store.get_channel_member_details(channel_a_id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_members_eq(
|
||||
&members,
|
||||
&[
|
||||
(
|
||||
client_a.user_id().unwrap(),
|
||||
true,
|
||||
proto::channel_member::Kind::Member,
|
||||
),
|
||||
(
|
||||
client_b.user_id().unwrap(),
|
||||
false,
|
||||
proto::channel_member::Kind::Invitee,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Client B accepts the invitation.
|
||||
client_b
|
||||
.channel_store()
|
||||
.update(cx_b, |channels, _| {
|
||||
channels.respond_to_channel_invite(channel_a_id, true)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Client B now sees that they are a member of channel A and its existing subchannels.
|
||||
assert_channel_invitations(client_b.channel_store(), cx_b, &[]);
|
||||
assert_channels(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
user_is_admin: false,
|
||||
depth: 0,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
user_is_admin: false,
|
||||
depth: 1,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
let channel_c_id = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.create_channel("channel-c", Some(channel_b_id), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
assert_channels(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
user_is_admin: false,
|
||||
depth: 0,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
user_is_admin: false,
|
||||
depth: 1,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_c_id,
|
||||
name: "channel-c".to_string(),
|
||||
user_is_admin: false,
|
||||
depth: 2,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
// Update client B's membership to channel A to be an admin.
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| {
|
||||
store.set_member_admin(channel_a_id, client_b.user_id().unwrap(), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Observe that client B is now an admin of channel A, and that
|
||||
// their admin priveleges extend to subchannels of channel A.
|
||||
assert_channel_invitations(client_b.channel_store(), cx_b, &[]);
|
||||
assert_channels(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
depth: 0,
|
||||
user_is_admin: true,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
depth: 1,
|
||||
user_is_admin: true,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_c_id,
|
||||
name: "channel-c".to_string(),
|
||||
depth: 2,
|
||||
user_is_admin: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
// Client A deletes the channel, deletion also deletes subchannels.
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, _| {
|
||||
channel_store.remove_channel(channel_b_id)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
assert_channels(
|
||||
client_a.channel_store(),
|
||||
cx_a,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
depth: 0,
|
||||
user_is_admin: true,
|
||||
}],
|
||||
);
|
||||
assert_channels(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
depth: 0,
|
||||
user_is_admin: true,
|
||||
}],
|
||||
);
|
||||
|
||||
// Remove client B
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.remove_member(channel_a_id, client_b.user_id().unwrap(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Client A still has their channel
|
||||
assert_channels(
|
||||
client_a.channel_store(),
|
||||
cx_a,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
depth: 0,
|
||||
user_is_admin: true,
|
||||
}],
|
||||
);
|
||||
|
||||
// Client B no longer has access to the channel
|
||||
assert_channels(client_b.channel_store(), cx_b, &[]);
|
||||
|
||||
// When disconnected, client A sees no channels.
|
||||
server.forbid_connections();
|
||||
server.disconnect_client(client_a.peer_id().unwrap());
|
||||
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
|
||||
assert_channels(client_a.channel_store(), cx_a, &[]);
|
||||
|
||||
server.allow_connections();
|
||||
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
|
||||
assert_channels(
|
||||
client_a.channel_store(),
|
||||
cx_a,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
depth: 0,
|
||||
user_is_admin: true,
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_participants_eq(participants: &[Arc<User>], expected_partitipants: &[u64]) {
|
||||
assert_eq!(
|
||||
participants.iter().map(|p| p.id).collect::<Vec<_>>(),
|
||||
expected_partitipants
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_members_eq(
|
||||
members: &[ChannelMembership],
|
||||
expected_members: &[(u64, bool, proto::channel_member::Kind)],
|
||||
) {
|
||||
assert_eq!(
|
||||
members
|
||||
.iter()
|
||||
.map(|member| (member.user.id, member.admin, member.kind))
|
||||
.collect::<Vec<_>>(),
|
||||
expected_members
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_joining_channel_ancestor_member(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let parent_id = server
|
||||
.make_channel("parent", (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
let sub_id = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.create_channel("sub_channel", Some(parent_id), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
assert!(active_call_b
|
||||
.update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx))
|
||||
.await
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_room(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
|
||||
let zed_id = server
|
||||
.make_channel(
|
||||
"zed",
|
||||
(&client_a, cx_a),
|
||||
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||
)
|
||||
.await;
|
||||
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Give everyone a chance to observe user A joining
|
||||
deterministic.run_until_parked();
|
||||
|
||||
client_a.channel_store().read_with(cx_a, |channels, _| {
|
||||
assert_participants_eq(
|
||||
channels.channel_participants(zed_id),
|
||||
&[client_a.user_id().unwrap()],
|
||||
);
|
||||
});
|
||||
|
||||
assert_channels(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
id: zed_id,
|
||||
name: "zed".to_string(),
|
||||
depth: 0,
|
||||
user_is_admin: false,
|
||||
}],
|
||||
);
|
||||
client_b.channel_store().read_with(cx_b, |channels, _| {
|
||||
assert_participants_eq(
|
||||
channels.channel_participants(zed_id),
|
||||
&[client_a.user_id().unwrap()],
|
||||
);
|
||||
});
|
||||
|
||||
client_c.channel_store().read_with(cx_c, |channels, _| {
|
||||
assert_participants_eq(
|
||||
channels.channel_participants(zed_id),
|
||||
&[client_a.user_id().unwrap()],
|
||||
);
|
||||
});
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
client_a.channel_store().read_with(cx_a, |channels, _| {
|
||||
assert_participants_eq(
|
||||
channels.channel_participants(zed_id),
|
||||
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
|
||||
);
|
||||
});
|
||||
|
||||
client_b.channel_store().read_with(cx_b, |channels, _| {
|
||||
assert_participants_eq(
|
||||
channels.channel_participants(zed_id),
|
||||
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
|
||||
);
|
||||
});
|
||||
|
||||
client_c.channel_store().read_with(cx_c, |channels, _| {
|
||||
assert_participants_eq(
|
||||
channels.channel_participants(zed_id),
|
||||
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
|
||||
);
|
||||
});
|
||||
|
||||
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
|
||||
room_a.read_with(cx_a, |room, _| assert!(room.is_connected()));
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
RoomParticipants {
|
||||
remote: vec!["user_b".to_string()],
|
||||
pending: vec![]
|
||||
}
|
||||
);
|
||||
|
||||
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
|
||||
room_b.read_with(cx_b, |room, _| assert!(room.is_connected()));
|
||||
assert_eq!(
|
||||
room_participants(&room_b, cx_b),
|
||||
RoomParticipants {
|
||||
remote: vec!["user_a".to_string()],
|
||||
pending: vec![]
|
||||
}
|
||||
);
|
||||
|
||||
// Make sure that leaving and rejoining works
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |active_call, cx| active_call.hang_up(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
client_a.channel_store().read_with(cx_a, |channels, _| {
|
||||
assert_participants_eq(
|
||||
channels.channel_participants(zed_id),
|
||||
&[client_b.user_id().unwrap()],
|
||||
);
|
||||
});
|
||||
|
||||
client_b.channel_store().read_with(cx_b, |channels, _| {
|
||||
assert_participants_eq(
|
||||
channels.channel_participants(zed_id),
|
||||
&[client_b.user_id().unwrap()],
|
||||
);
|
||||
});
|
||||
|
||||
client_c.channel_store().read_with(cx_c, |channels, _| {
|
||||
assert_participants_eq(
|
||||
channels.channel_participants(zed_id),
|
||||
&[client_b.user_id().unwrap()],
|
||||
);
|
||||
});
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |active_call, cx| active_call.hang_up(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
client_a.channel_store().read_with(cx_a, |channels, _| {
|
||||
assert_participants_eq(channels.channel_participants(zed_id), &[]);
|
||||
});
|
||||
|
||||
client_b.channel_store().read_with(cx_b, |channels, _| {
|
||||
assert_participants_eq(channels.channel_participants(zed_id), &[]);
|
||||
});
|
||||
|
||||
client_c.channel_store().read_with(cx_c, |channels, _| {
|
||||
assert_participants_eq(channels.channel_participants(zed_id), &[]);
|
||||
});
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
|
||||
room_a.read_with(cx_a, |room, _| assert!(room.is_connected()));
|
||||
assert_eq!(
|
||||
room_participants(&room_a, cx_a),
|
||||
RoomParticipants {
|
||||
remote: vec!["user_b".to_string()],
|
||||
pending: vec![]
|
||||
}
|
||||
);
|
||||
|
||||
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
|
||||
room_b.read_with(cx_b, |room, _| assert!(room.is_connected()));
|
||||
assert_eq!(
|
||||
room_participants(&room_b, cx_b),
|
||||
RoomParticipants {
|
||||
remote: vec!["user_a".to_string()],
|
||||
pending: vec![]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_jumping(deterministic: Arc<Deterministic>, cx_a: &mut TestAppContext) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
|
||||
let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await;
|
||||
let rust_id = server
|
||||
.make_channel("rust", (&client_a, cx_a), &mut [])
|
||||
.await;
|
||||
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Give everything a chance to observe user A joining
|
||||
deterministic.run_until_parked();
|
||||
|
||||
client_a.channel_store().read_with(cx_a, |channels, _| {
|
||||
assert_participants_eq(
|
||||
channels.channel_participants(zed_id),
|
||||
&[client_a.user_id().unwrap()],
|
||||
);
|
||||
assert_participants_eq(channels.channel_participants(rust_id), &[]);
|
||||
});
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |active_call, cx| {
|
||||
active_call.join_channel(rust_id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
client_a.channel_store().read_with(cx_a, |channels, _| {
|
||||
assert_participants_eq(channels.channel_participants(zed_id), &[]);
|
||||
assert_participants_eq(
|
||||
channels.channel_participants(rust_id),
|
||||
&[client_a.user_id().unwrap()],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_permissions_update_while_invited(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let rust_id = server
|
||||
.make_channel("rust", (&client_a, cx_a), &mut [])
|
||||
.await;
|
||||
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.invite_member(rust_id, client_b.user_id().unwrap(), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
assert_channel_invitations(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust".to_string(),
|
||||
user_is_admin: false,
|
||||
}],
|
||||
);
|
||||
assert_channels(client_b.channel_store(), cx_b, &[]);
|
||||
|
||||
// Update B's invite before they've accepted it
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.set_member_admin(rust_id, client_b.user_id().unwrap(), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
assert_channel_invitations(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust".to_string(),
|
||||
user_is_admin: false,
|
||||
}],
|
||||
);
|
||||
assert_channels(client_b.channel_store(), cx_b, &[]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_rename(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let rust_id = server
|
||||
.make_channel("rust", (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
// Rename the channel
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.rename(rust_id, "#rust-archive", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Client A sees the channel with its new name.
|
||||
assert_channels(
|
||||
client_a.channel_store(),
|
||||
cx_a,
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust-archive".to_string(),
|
||||
user_is_admin: true,
|
||||
}],
|
||||
);
|
||||
|
||||
// Client B sees the channel with its new name.
|
||||
assert_channels(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust-archive".to_string(),
|
||||
user_is_admin: false,
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_call_from_channel(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
server
|
||||
.make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
let channel_id = server
|
||||
.make_channel(
|
||||
"x",
|
||||
(&client_a, cx_a),
|
||||
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||
)
|
||||
.await;
|
||||
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A calls client B while in the channel.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_b.user_id().unwrap(), None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client B accepts the call.
|
||||
deterministic.run_until_parked();
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.accept_incoming(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client B sees that they are now in the channel
|
||||
deterministic.run_until_parked();
|
||||
active_call_b.read_with(cx_b, |call, cx| {
|
||||
assert_eq!(call.channel_id(cx), Some(channel_id));
|
||||
});
|
||||
client_b.channel_store().read_with(cx_b, |channels, _| {
|
||||
assert_participants_eq(
|
||||
channels.channel_participants(channel_id),
|
||||
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
|
||||
);
|
||||
});
|
||||
|
||||
// Clients A and C also see that client B is in the channel.
|
||||
client_a.channel_store().read_with(cx_a, |channels, _| {
|
||||
assert_participants_eq(
|
||||
channels.channel_participants(channel_id),
|
||||
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
|
||||
);
|
||||
});
|
||||
client_c.channel_store().read_with(cx_c, |channels, _| {
|
||||
assert_participants_eq(
|
||||
channels.channel_participants(channel_id),
|
||||
&[client_a.user_id().unwrap(), client_b.user_id().unwrap()],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_lost_channel_creation(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
server
|
||||
.make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
let channel_id = server.make_channel("x", (&client_a, cx_a), &mut []).await;
|
||||
|
||||
// Invite a member
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.invite_member(channel_id, client_b.user_id().unwrap(), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Sanity check
|
||||
assert_channel_invitations(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".to_string(),
|
||||
user_is_admin: false,
|
||||
}],
|
||||
);
|
||||
|
||||
let subchannel_id = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.create_channel("subchannel", Some(channel_id), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Make sure A sees their new channel
|
||||
assert_channels(
|
||||
client_a.channel_store(),
|
||||
cx_a,
|
||||
&[
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".to_string(),
|
||||
user_is_admin: true,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: subchannel_id,
|
||||
name: "subchannel".to_string(),
|
||||
user_is_admin: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
// Accept the invite
|
||||
client_b
|
||||
.channel_store()
|
||||
.update(cx_b, |channel_store, _| {
|
||||
channel_store.respond_to_channel_invite(channel_id, true)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// B should now see the channel
|
||||
assert_channels(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".to_string(),
|
||||
user_is_admin: false,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: subchannel_id,
|
||||
name: "subchannel".to_string(),
|
||||
user_is_admin: false,
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct ExpectedChannel {
|
||||
depth: usize,
|
||||
id: ChannelId,
|
||||
name: String,
|
||||
user_is_admin: bool,
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_channel_invitations(
|
||||
channel_store: &ModelHandle<ChannelStore>,
|
||||
cx: &TestAppContext,
|
||||
expected_channels: &[ExpectedChannel],
|
||||
) {
|
||||
let actual = channel_store.read_with(cx, |store, _| {
|
||||
store
|
||||
.channel_invitations()
|
||||
.iter()
|
||||
.map(|channel| ExpectedChannel {
|
||||
depth: 0,
|
||||
name: channel.name.clone(),
|
||||
id: channel.id,
|
||||
user_is_admin: store.is_user_admin(channel.id),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(actual, expected_channels);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_channels(
|
||||
channel_store: &ModelHandle<ChannelStore>,
|
||||
cx: &TestAppContext,
|
||||
expected_channels: &[ExpectedChannel],
|
||||
) {
|
||||
let actual = channel_store.read_with(cx, |store, _| {
|
||||
store
|
||||
.channels()
|
||||
.map(|(depth, channel)| ExpectedChannel {
|
||||
depth,
|
||||
name: channel.name.clone(),
|
||||
id: channel.id,
|
||||
user_is_admin: store.is_user_admin(channel.id),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(actual, expected_channels);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
||||
tests::{TestClient, TestServer},
|
||||
tests::{room_participants, RoomParticipants, TestClient, TestServer},
|
||||
};
|
||||
use call::{room, ActiveCall, ParticipantLocation, Room};
|
||||
use client::{User, RECEIVE_TIMEOUT};
|
||||
|
@ -748,7 +748,7 @@ async fn test_server_restarts(
|
|||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree("/a", json!({ "a.txt": "a-contents" }))
|
||||
.await;
|
||||
|
||||
|
@ -1220,7 +1220,7 @@ async fn test_share_project(
|
|||
let active_call_c = cx_c.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
|
@ -1387,7 +1387,7 @@ async fn test_unshare_project(
|
|||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
|
@ -1476,7 +1476,7 @@ async fn test_host_disconnect(
|
|||
cx_b.update(editor::init);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
|
@ -1498,7 +1498,8 @@ async fn test_host_disconnect(
|
|||
deterministic.run_until_parked();
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
||||
|
||||
let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
|
||||
let window_b =
|
||||
cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
|
||||
let workspace_b = window_b.root(cx_b);
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
|
@ -1581,7 +1582,7 @@ async fn test_project_reconnect(
|
|||
cx_b.update(editor::init);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/root-1",
|
||||
json!({
|
||||
|
@ -1609,7 +1610,7 @@ async fn test_project_reconnect(
|
|||
)
|
||||
.await;
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/root-2",
|
||||
json!({
|
||||
|
@ -1618,7 +1619,7 @@ async fn test_project_reconnect(
|
|||
)
|
||||
.await;
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/root-3",
|
||||
json!({
|
||||
|
@ -1698,7 +1699,7 @@ async fn test_project_reconnect(
|
|||
|
||||
// While client A is disconnected, add and remove files from client A's project.
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/root-1/dir1/subdir2",
|
||||
json!({
|
||||
|
@ -1710,7 +1711,7 @@ async fn test_project_reconnect(
|
|||
)
|
||||
.await;
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.remove_dir(
|
||||
"/root-1/dir1/subdir1".as_ref(),
|
||||
RemoveOptions {
|
||||
|
@ -1832,11 +1833,11 @@ async fn test_project_reconnect(
|
|||
|
||||
// While client B is disconnected, add and remove files from client A's project
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_file("/root-1/dir1/subdir2/j.txt", "j-contents".into())
|
||||
.await;
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.remove_file("/root-1/dir1/subdir2/i.txt".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -1922,8 +1923,8 @@ async fn test_active_call_events(
|
|||
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;
|
||||
client_a.fs.insert_tree("/a", json!({})).await;
|
||||
client_b.fs.insert_tree("/b", json!({})).await;
|
||||
client_a.fs().insert_tree("/a", json!({})).await;
|
||||
client_b.fs().insert_tree("/b", 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;
|
||||
|
@ -2011,8 +2012,8 @@ async fn test_room_location(
|
|||
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;
|
||||
client_a.fs.insert_tree("/a", json!({})).await;
|
||||
client_b.fs.insert_tree("/b", json!({})).await;
|
||||
client_a.fs().insert_tree("/a", json!({})).await;
|
||||
client_b.fs().insert_tree("/b", json!({})).await;
|
||||
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
@ -2201,12 +2202,12 @@ async fn test_propagate_saves_and_fs_changes(
|
|||
Some(tree_sitter_rust::language()),
|
||||
));
|
||||
for client in [&client_a, &client_b, &client_c] {
|
||||
client.language_registry.add(rust.clone());
|
||||
client.language_registry.add(javascript.clone());
|
||||
client.language_registry().add(rust.clone());
|
||||
client.language_registry().add(javascript.clone());
|
||||
}
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
|
@ -2276,7 +2277,7 @@ async fn test_propagate_saves_and_fs_changes(
|
|||
buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
|
||||
save_b.await.unwrap();
|
||||
assert_eq!(
|
||||
client_a.fs.load("/a/file1.rs".as_ref()).await.unwrap(),
|
||||
client_a.fs().load("/a/file1.rs".as_ref()).await.unwrap(),
|
||||
"hi-a, i-am-c, i-am-b, i-am-a"
|
||||
);
|
||||
|
||||
|
@ -2287,7 +2288,7 @@ async fn test_propagate_saves_and_fs_changes(
|
|||
|
||||
// Make changes on host's file system, see those changes on guest worktrees.
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.rename(
|
||||
"/a/file1.rs".as_ref(),
|
||||
"/a/file1.js".as_ref(),
|
||||
|
@ -2296,11 +2297,11 @@ async fn test_propagate_saves_and_fs_changes(
|
|||
.await
|
||||
.unwrap();
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
client_a.fs.insert_file("/a/file4", "4".into()).await;
|
||||
client_a.fs().insert_file("/a/file4", "4".into()).await;
|
||||
deterministic.run_until_parked();
|
||||
|
||||
worktree_a.read_with(cx_a, |tree, _| {
|
||||
|
@ -2394,7 +2395,7 @@ async fn test_git_diff_base_change(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
|
@ -2438,7 +2439,7 @@ async fn test_git_diff_base_change(
|
|||
"
|
||||
.unindent();
|
||||
|
||||
client_a.fs.as_fake().set_index_for_repo(
|
||||
client_a.fs().set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), diff_base.clone())],
|
||||
);
|
||||
|
@ -2483,7 +2484,7 @@ async fn test_git_diff_base_change(
|
|||
);
|
||||
});
|
||||
|
||||
client_a.fs.as_fake().set_index_for_repo(
|
||||
client_a.fs().set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), new_diff_base.clone())],
|
||||
);
|
||||
|
@ -2528,7 +2529,7 @@ async fn test_git_diff_base_change(
|
|||
"
|
||||
.unindent();
|
||||
|
||||
client_a.fs.as_fake().set_index_for_repo(
|
||||
client_a.fs().set_index_for_repo(
|
||||
Path::new("/dir/sub/.git"),
|
||||
&[(Path::new("b.txt"), diff_base.clone())],
|
||||
);
|
||||
|
@ -2573,7 +2574,7 @@ async fn test_git_diff_base_change(
|
|||
);
|
||||
});
|
||||
|
||||
client_a.fs.as_fake().set_index_for_repo(
|
||||
client_a.fs().set_index_for_repo(
|
||||
Path::new("/dir/sub/.git"),
|
||||
&[(Path::new("b.txt"), new_diff_base.clone())],
|
||||
);
|
||||
|
@ -2632,7 +2633,7 @@ async fn test_git_branch_name(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
|
@ -2651,8 +2652,7 @@ async fn test_git_branch_name(
|
|||
|
||||
let project_remote = client_b.build_remote_project(project_id, cx_b).await;
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.fs()
|
||||
.set_branch_name(Path::new("/dir/.git"), Some("branch-1"));
|
||||
|
||||
// Wait for it to catch up to the new branch
|
||||
|
@ -2677,8 +2677,7 @@ async fn test_git_branch_name(
|
|||
});
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.fs()
|
||||
.set_branch_name(Path::new("/dir/.git"), Some("branch-2"));
|
||||
|
||||
// Wait for buffer_local_a to receive it
|
||||
|
@ -2717,7 +2716,7 @@ async fn test_git_status_sync(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
|
@ -2731,7 +2730,7 @@ async fn test_git_status_sync(
|
|||
const A_TXT: &'static str = "a.txt";
|
||||
const B_TXT: &'static str = "b.txt";
|
||||
|
||||
client_a.fs.as_fake().set_status_for_repo_via_git_operation(
|
||||
client_a.fs().set_status_for_repo_via_git_operation(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(&Path::new(A_TXT), GitFileStatus::Added),
|
||||
|
@ -2777,16 +2776,13 @@ async fn test_git_status_sync(
|
|||
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
});
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_status_for_repo_via_working_copy_change(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(&Path::new(A_TXT), GitFileStatus::Modified),
|
||||
(&Path::new(B_TXT), GitFileStatus::Modified),
|
||||
],
|
||||
);
|
||||
client_a.fs().set_status_for_repo_via_working_copy_change(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(&Path::new(A_TXT), GitFileStatus::Modified),
|
||||
(&Path::new(B_TXT), GitFileStatus::Modified),
|
||||
],
|
||||
);
|
||||
|
||||
// Wait for buffer_local_a to receive it
|
||||
deterministic.run_until_parked();
|
||||
|
@ -2857,7 +2853,7 @@ async fn test_fs_operations(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
|
@ -3130,7 +3126,7 @@ async fn test_local_settings(
|
|||
|
||||
// As client A, open a project that contains some local settings files
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
|
@ -3172,7 +3168,7 @@ async fn test_local_settings(
|
|||
|
||||
// As client A, update a settings file. As Client B, see the changed settings.
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_file("/dir/.zed/settings.json", r#"{}"#.into())
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
|
@ -3189,17 +3185,17 @@ async fn test_local_settings(
|
|||
|
||||
// As client A, create and remove some settings files. As client B, see the changed settings.
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.create_dir("/dir/b/.zed".as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
|
@ -3220,11 +3216,11 @@ async fn test_local_settings(
|
|||
|
||||
// As client A, change and remove settings files while client B is disconnected.
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
|
||||
.await;
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -3258,7 +3254,7 @@ async fn test_buffer_conflict_after_save(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
|
@ -3320,7 +3316,7 @@ async fn test_buffer_reloading(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
|
@ -3348,7 +3344,7 @@ async fn test_buffer_reloading(
|
|||
|
||||
let new_contents = Rope::from("d\ne\nf");
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -3377,7 +3373,7 @@ async fn test_editing_while_guest_opens_buffer(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree("/dir", json!({ "a.txt": "a-contents" }))
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||
|
@ -3426,7 +3422,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||
|
@ -3520,7 +3516,7 @@ async fn test_leaving_worktree_while_opening_buffer(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree("/dir", json!({ "a.txt": "a-contents" }))
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||
|
@ -3563,7 +3559,7 @@ async fn test_canceling_buffer_opening(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
|
@ -3619,7 +3615,7 @@ async fn test_leaving_project(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
|
@ -3707,9 +3703,9 @@ async fn test_leaving_project(
|
|||
cx_b.spawn(|cx| {
|
||||
Project::remote(
|
||||
project_id,
|
||||
client_b.client.clone(),
|
||||
client_b.user_store.clone(),
|
||||
client_b.language_registry.clone(),
|
||||
client_b.app_state.client.clone(),
|
||||
client_b.user_store().clone(),
|
||||
client_b.language_registry().clone(),
|
||||
FakeFs::new(cx.background()),
|
||||
cx,
|
||||
)
|
||||
|
@ -3761,11 +3757,11 @@ async fn test_collaborating_with_diagnostics(
|
|||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
// Share a project as client A
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
|
@ -4033,11 +4029,11 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
|||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
|
@ -4174,10 +4170,10 @@ async fn test_collaborating_with_completion(
|
|||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
|
@ -4335,7 +4331,7 @@ async fn test_reloading_buffer_manually(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
|
@ -4366,7 +4362,7 @@ async fn test_reloading_buffer_manually(
|
|||
buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.save(
|
||||
"/a/a.rs".as_ref(),
|
||||
&Rope::from("let seven = 7;"),
|
||||
|
@ -4437,14 +4433,14 @@ async fn test_formatting_buffer(
|
|||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
// Here we insert a fake tree with a directory that exists on disk. This is needed
|
||||
// because later we'll invoke a command, which requires passing a working directory
|
||||
// that points to a valid location on disk.
|
||||
let directory = env::current_dir().unwrap();
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
|
||||
|
@ -4546,10 +4542,10 @@ async fn test_definition(
|
|||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
|
@ -4694,10 +4690,10 @@ async fn test_references(
|
|||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
|
@ -4790,7 +4786,7 @@ async fn test_project_search(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
|
@ -4876,7 +4872,7 @@ async fn test_document_highlights(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/root-1",
|
||||
json!({
|
||||
|
@ -4895,7 +4891,7 @@ async fn test_document_highlights(
|
|||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
|
@ -4982,7 +4978,7 @@ async fn test_lsp_hover(
|
|||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/root-1",
|
||||
json!({
|
||||
|
@ -5001,7 +4997,7 @@ async fn test_lsp_hover(
|
|||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
|
@ -5100,10 +5096,10 @@ async fn test_project_symbols(
|
|||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/code",
|
||||
json!({
|
||||
|
@ -5211,10 +5207,10 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
|||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
|
@ -5271,6 +5267,7 @@ async fn test_collaborating_with_code_actions(
|
|||
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;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
|
@ -5289,10 +5286,10 @@ async fn test_collaborating_with_code_actions(
|
|||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
|
@ -5309,7 +5306,8 @@ async fn test_collaborating_with_code_actions(
|
|||
|
||||
// Join the project as client B.
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
|
||||
let window_b =
|
||||
cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
|
||||
let workspace_b = window_b.root(cx_b);
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
|
@ -5515,10 +5513,10 @@ async fn test_collaborating_with_renames(
|
|||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
|
@ -5534,7 +5532,8 @@ async fn test_collaborating_with_renames(
|
|||
.unwrap();
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
|
||||
let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
|
||||
let window_b =
|
||||
cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
|
||||
let workspace_b = window_b.root(cx_b);
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
|
@ -5702,10 +5701,10 @@ async fn test_language_server_statuses(
|
|||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
|
@ -6162,7 +6161,7 @@ async fn test_contacts(
|
|||
|
||||
// Test removing a contact
|
||||
client_b
|
||||
.user_store
|
||||
.user_store()
|
||||
.update(cx_b, |store, cx| {
|
||||
store.remove_contact(client_c.user_id().unwrap(), cx)
|
||||
})
|
||||
|
@ -6185,7 +6184,7 @@ async fn test_contacts(
|
|||
client: &TestClient,
|
||||
cx: &TestAppContext,
|
||||
) -> Vec<(String, &'static str, &'static str)> {
|
||||
client.user_store.read_with(cx, |store, _| {
|
||||
client.user_store().read_with(cx, |store, _| {
|
||||
store
|
||||
.contacts()
|
||||
.iter()
|
||||
|
@ -6228,14 +6227,14 @@ async fn test_contact_requests(
|
|||
|
||||
// User A and User C request that user B become their contact.
|
||||
client_a
|
||||
.user_store
|
||||
.user_store()
|
||||
.update(cx_a, |store, cx| {
|
||||
store.request_contact(client_b.user_id().unwrap(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
client_c
|
||||
.user_store
|
||||
.user_store()
|
||||
.update(cx_c, |store, cx| {
|
||||
store.request_contact(client_b.user_id().unwrap(), cx)
|
||||
})
|
||||
|
@ -6289,7 +6288,7 @@ async fn test_contact_requests(
|
|||
|
||||
// User B accepts the request from user A.
|
||||
client_b
|
||||
.user_store
|
||||
.user_store()
|
||||
.update(cx_b, |store, cx| {
|
||||
store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
|
||||
})
|
||||
|
@ -6333,7 +6332,7 @@ async fn test_contact_requests(
|
|||
|
||||
// User B rejects the request from user C.
|
||||
client_b
|
||||
.user_store
|
||||
.user_store()
|
||||
.update(cx_b, |store, cx| {
|
||||
store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
|
||||
})
|
||||
|
@ -6415,7 +6414,7 @@ async fn test_basic_following(
|
|||
cx_b.update(editor::init);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
|
@ -6978,7 +6977,7 @@ async fn test_join_call_after_screen_was_shared(
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
client_b.user_store.update(cx_b, |user_store, _| {
|
||||
client_b.user_store().update(cx_b, |user_store, _| {
|
||||
user_store.clear_cache();
|
||||
});
|
||||
|
||||
|
@ -7038,7 +7037,7 @@ async fn test_following_tab_order(
|
|||
cx_b.update(editor::init);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
|
@ -7161,7 +7160,7 @@ async fn test_peers_following_each_other(
|
|||
|
||||
// Client A shares a project.
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
|
@ -7334,7 +7333,7 @@ async fn test_auto_unfollowing(
|
|||
|
||||
// Client A shares a project.
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
|
@ -7498,7 +7497,7 @@ async fn test_peers_simultaneously_following_each_other(
|
|||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
client_a.fs.insert_tree("/a", json!({})).await;
|
||||
client_a.fs().insert_tree("/a", json!({})).await;
|
||||
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||
let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
let project_id = active_call_a
|
||||
|
@ -7575,10 +7574,10 @@ async fn test_on_input_format_from_host_to_guest(
|
|||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
|
@ -7704,10 +7703,10 @@ async fn test_on_input_format_from_guest_to_host(
|
|||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
|
@ -7860,15 +7859,15 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
|||
}))
|
||||
.await;
|
||||
let language = Arc::new(language);
|
||||
client_a.language_registry.add(Arc::clone(&language));
|
||||
client_b.language_registry.add(language);
|
||||
client_a.language_registry().add(Arc::clone(&language));
|
||||
client_b.language_registry().add(language);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
|
||||
"main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
|
||||
"other.rs": "// Test file",
|
||||
}),
|
||||
)
|
||||
|
@ -8170,15 +8169,15 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
|||
}))
|
||||
.await;
|
||||
let language = Arc::new(language);
|
||||
client_a.language_registry.add(Arc::clone(&language));
|
||||
client_b.language_registry.add(language);
|
||||
client_a.language_registry().add(Arc::clone(&language));
|
||||
client_b.language_registry().add(language);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
|
||||
"main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
|
||||
"other.rs": "// Test file",
|
||||
}),
|
||||
)
|
||||
|
@ -8324,30 +8323,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
|||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
struct RoomParticipants {
|
||||
remote: Vec<String>,
|
||||
pending: Vec<String>,
|
||||
}
|
||||
|
||||
fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
|
||||
room.read_with(cx, |room, _| {
|
||||
let mut remote = room
|
||||
.remote_participants()
|
||||
.iter()
|
||||
.map(|(_, participant)| participant.user.github_login.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let mut pending = room
|
||||
.pending_participants()
|
||||
.iter()
|
||||
.map(|user| user.github_login.clone())
|
||||
.collect::<Vec<_>>();
|
||||
remote.sort();
|
||||
pending.sort();
|
||||
RoomParticipants { remote, pending }
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_hint_labels(editor: &Editor) -> Vec<String> {
|
||||
let mut labels = Vec::new();
|
||||
for hint in editor.inlay_hint_cache().hints() {
|
||||
|
|
|
@ -396,9 +396,9 @@ async fn apply_client_operation(
|
|||
);
|
||||
|
||||
let root_path = Path::new("/").join(&first_root_name);
|
||||
client.fs.create_dir(&root_path).await.unwrap();
|
||||
client.fs().create_dir(&root_path).await.unwrap();
|
||||
client
|
||||
.fs
|
||||
.fs()
|
||||
.create_file(&root_path.join("main.rs"), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -422,8 +422,8 @@ async fn apply_client_operation(
|
|||
);
|
||||
|
||||
ensure_project_shared(&project, client, cx).await;
|
||||
if !client.fs.paths(false).contains(&new_root_path) {
|
||||
client.fs.create_dir(&new_root_path).await.unwrap();
|
||||
if !client.fs().paths(false).contains(&new_root_path) {
|
||||
client.fs().create_dir(&new_root_path).await.unwrap();
|
||||
}
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
|
@ -475,7 +475,7 @@ async fn apply_client_operation(
|
|||
Some(room.update(cx, |room, cx| {
|
||||
room.join_project(
|
||||
project_id,
|
||||
client.language_registry.clone(),
|
||||
client.language_registry().clone(),
|
||||
FakeFs::new(cx.background().clone()),
|
||||
cx,
|
||||
)
|
||||
|
@ -743,7 +743,7 @@ async fn apply_client_operation(
|
|||
content,
|
||||
} => {
|
||||
if !client
|
||||
.fs
|
||||
.fs()
|
||||
.directories(false)
|
||||
.contains(&path.parent().unwrap().to_owned())
|
||||
{
|
||||
|
@ -752,14 +752,14 @@ async fn apply_client_operation(
|
|||
|
||||
if is_dir {
|
||||
log::info!("{}: creating dir at {:?}", client.username, path);
|
||||
client.fs.create_dir(&path).await.unwrap();
|
||||
client.fs().create_dir(&path).await.unwrap();
|
||||
} else {
|
||||
let exists = client.fs.metadata(&path).await?.is_some();
|
||||
let exists = client.fs().metadata(&path).await?.is_some();
|
||||
let verb = if exists { "updating" } else { "creating" };
|
||||
log::info!("{}: {} file at {:?}", verb, client.username, path);
|
||||
|
||||
client
|
||||
.fs
|
||||
.fs()
|
||||
.save(&path, &content.as_str().into(), fs::LineEnding::Unix)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -771,12 +771,12 @@ async fn apply_client_operation(
|
|||
repo_path,
|
||||
contents,
|
||||
} => {
|
||||
if !client.fs.directories(false).contains(&repo_path) {
|
||||
if !client.fs().directories(false).contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
|
||||
for (path, _) in contents.iter() {
|
||||
if !client.fs.files().contains(&repo_path.join(path)) {
|
||||
if !client.fs().files().contains(&repo_path.join(path)) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
}
|
||||
|
@ -793,16 +793,16 @@ async fn apply_client_operation(
|
|||
.iter()
|
||||
.map(|(path, contents)| (path.as_path(), contents.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
if client.fs().metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs().create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
client.fs.set_index_for_repo(&dot_git_dir, &contents);
|
||||
client.fs().set_index_for_repo(&dot_git_dir, &contents);
|
||||
}
|
||||
GitOperation::WriteGitBranch {
|
||||
repo_path,
|
||||
new_branch,
|
||||
} => {
|
||||
if !client.fs.directories(false).contains(&repo_path) {
|
||||
if !client.fs().directories(false).contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
|
||||
|
@ -814,21 +814,21 @@ async fn apply_client_operation(
|
|||
);
|
||||
|
||||
let dot_git_dir = repo_path.join(".git");
|
||||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
if client.fs().metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs().create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
client.fs.set_branch_name(&dot_git_dir, new_branch);
|
||||
client.fs().set_branch_name(&dot_git_dir, new_branch);
|
||||
}
|
||||
GitOperation::WriteGitStatuses {
|
||||
repo_path,
|
||||
statuses,
|
||||
git_operation,
|
||||
} => {
|
||||
if !client.fs.directories(false).contains(&repo_path) {
|
||||
if !client.fs().directories(false).contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
for (path, _) in statuses.iter() {
|
||||
if !client.fs.files().contains(&repo_path.join(path)) {
|
||||
if !client.fs().files().contains(&repo_path.join(path)) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
}
|
||||
|
@ -847,16 +847,16 @@ async fn apply_client_operation(
|
|||
.map(|(path, val)| (path.as_path(), val.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
if client.fs().metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs().create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
|
||||
if git_operation {
|
||||
client
|
||||
.fs
|
||||
.fs()
|
||||
.set_status_for_repo_via_git_operation(&dot_git_dir, statuses.as_slice());
|
||||
} else {
|
||||
client.fs.set_status_for_repo_via_working_copy_change(
|
||||
client.fs().set_status_for_repo_via_working_copy_change(
|
||||
&dot_git_dir,
|
||||
statuses.as_slice(),
|
||||
);
|
||||
|
@ -1499,7 +1499,7 @@ impl TestPlan {
|
|||
// Invite a contact to the current call
|
||||
0..=70 => {
|
||||
let available_contacts =
|
||||
client.user_store.read_with(cx, |user_store, _| {
|
||||
client.user_store().read_with(cx, |user_store, _| {
|
||||
user_store
|
||||
.contacts()
|
||||
.iter()
|
||||
|
@ -1596,7 +1596,7 @@ impl TestPlan {
|
|||
.choose(&mut self.rng)
|
||||
.cloned() else { continue };
|
||||
let project_root_name = root_name_for_project(&project, cx);
|
||||
let mut paths = client.fs.paths(false);
|
||||
let mut paths = client.fs().paths(false);
|
||||
paths.remove(0);
|
||||
let new_root_path = if paths.is_empty() || self.rng.gen() {
|
||||
Path::new("/").join(&self.next_root_dir_name(user_id))
|
||||
|
@ -1776,7 +1776,7 @@ impl TestPlan {
|
|||
let is_dir = self.rng.gen::<bool>();
|
||||
let content;
|
||||
let mut path;
|
||||
let dir_paths = client.fs.directories(false);
|
||||
let dir_paths = client.fs().directories(false);
|
||||
|
||||
if is_dir {
|
||||
content = String::new();
|
||||
|
@ -1786,7 +1786,7 @@ impl TestPlan {
|
|||
content = Alphanumeric.sample_string(&mut self.rng, 16);
|
||||
|
||||
// Create a new file or overwrite an existing file
|
||||
let file_paths = client.fs.files();
|
||||
let file_paths = client.fs().files();
|
||||
if file_paths.is_empty() || self.rng.gen_bool(0.5) {
|
||||
path = dir_paths.choose(&mut self.rng).unwrap().clone();
|
||||
path.push(gen_file_name(&mut self.rng));
|
||||
|
@ -1812,7 +1812,7 @@ impl TestPlan {
|
|||
client: &TestClient,
|
||||
) -> Vec<PathBuf> {
|
||||
let mut paths = client
|
||||
.fs
|
||||
.fs()
|
||||
.files()
|
||||
.into_iter()
|
||||
.filter(|path| path.starts_with(repo_path))
|
||||
|
@ -1829,7 +1829,7 @@ impl TestPlan {
|
|||
}
|
||||
|
||||
let repo_path = client
|
||||
.fs
|
||||
.fs()
|
||||
.directories(false)
|
||||
.choose(&mut self.rng)
|
||||
.unwrap()
|
||||
|
@ -1928,7 +1928,7 @@ async fn simulate_client(
|
|||
name: "the-fake-language-server",
|
||||
capabilities: lsp::LanguageServer::full_capabilities(),
|
||||
initializer: Some(Box::new({
|
||||
let fs = client.fs.clone();
|
||||
let fs = client.app_state.fs.clone();
|
||||
move |fake_server: &mut FakeLanguageServer| {
|
||||
fake_server.handle_request::<lsp::request::Completion, _, _>(
|
||||
|_, _| async move {
|
||||
|
@ -1973,7 +1973,7 @@ async fn simulate_client(
|
|||
let background = cx.background();
|
||||
let mut rng = background.rng();
|
||||
let count = rng.gen_range::<usize, _>(1..3);
|
||||
let files = fs.files();
|
||||
let files = fs.as_fake().files();
|
||||
let files = (0..count)
|
||||
.map(|_| files.choose(&mut *rng).unwrap().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
@ -2023,7 +2023,7 @@ async fn simulate_client(
|
|||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client.language_registry.add(Arc::new(language));
|
||||
client.app_state.languages.add(Arc::new(language));
|
||||
|
||||
while let Some(batch_id) = operation_rx.next().await {
|
||||
let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) else { break };
|
||||
|
|
|
@ -23,6 +23,7 @@ test-support = [
|
|||
|
||||
[dependencies]
|
||||
auto_update = { path = "../auto_update" }
|
||||
db = { path = "../db" }
|
||||
call = { path = "../call" }
|
||||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
|
@ -37,6 +38,7 @@ picker = { path = "../picker" }
|
|||
project = { path = "../project" }
|
||||
recent_projects = {path = "../recent_projects"}
|
||||
settings = { path = "../settings" }
|
||||
staff_mode = {path = "../staff_mode"}
|
||||
theme = { path = "../theme" }
|
||||
theme_selector = { path = "../theme_selector" }
|
||||
vcs_menu = { path = "../vcs_menu" }
|
||||
|
@ -44,10 +46,10 @@ util = { path = "../util" }
|
|||
workspace = { path = "../workspace" }
|
||||
zed-actions = {path = "../zed-actions"}
|
||||
|
||||
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
schemars.workspace = true
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
|
|
2521
crates/collab_ui/src/collab_panel.rs
Normal file
2521
crates/collab_ui/src/collab_panel.rs
Normal file
File diff suppressed because it is too large
Load diff
615
crates/collab_ui/src/collab_panel/channel_modal.rs
Normal file
615
crates/collab_ui/src/collab_panel/channel_modal.rs
Normal file
|
@ -0,0 +1,615 @@
|
|||
use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::sync::Arc;
|
||||
use util::TryFutureExt;
|
||||
use workspace::Modal;
|
||||
|
||||
actions!(
|
||||
channel_modal,
|
||||
[
|
||||
SelectNextControl,
|
||||
ToggleMode,
|
||||
ToggleMemberAdmin,
|
||||
RemoveMember
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Picker::<ChannelModalDelegate>::init(cx);
|
||||
cx.add_action(ChannelModal::toggle_mode);
|
||||
cx.add_action(ChannelModal::toggle_member_admin);
|
||||
cx.add_action(ChannelModal::remove_member);
|
||||
cx.add_action(ChannelModal::dismiss);
|
||||
}
|
||||
|
||||
pub struct ChannelModal {
|
||||
picker: ViewHandle<Picker<ChannelModalDelegate>>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
impl ChannelModal {
|
||||
pub fn new(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
mode: Mode,
|
||||
members: Vec<ChannelMembership>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
|
||||
let picker = cx.add_view(|cx| {
|
||||
Picker::new(
|
||||
ChannelModalDelegate {
|
||||
matching_users: Vec::new(),
|
||||
matching_member_indices: Vec::new(),
|
||||
selected_index: 0,
|
||||
user_store: user_store.clone(),
|
||||
channel_store: channel_store.clone(),
|
||||
channel_id,
|
||||
match_candidates: Vec::new(),
|
||||
members,
|
||||
mode,
|
||||
context_menu: cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(cx.view_id(), cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
}),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
|
||||
});
|
||||
|
||||
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
|
||||
|
||||
let has_focus = picker.read(cx).has_focus();
|
||||
|
||||
Self {
|
||||
picker,
|
||||
channel_store,
|
||||
channel_id,
|
||||
has_focus,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
|
||||
let mode = match self.picker.read(cx).delegate().mode {
|
||||
Mode::ManageMembers => Mode::InviteMembers,
|
||||
Mode::InviteMembers => Mode::ManageMembers,
|
||||
};
|
||||
self.set_mode(mode, cx);
|
||||
}
|
||||
|
||||
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
|
||||
let channel_store = self.channel_store.clone();
|
||||
let channel_id = self.channel_id;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if mode == Mode::ManageMembers {
|
||||
let members = channel_store
|
||||
.update(&mut cx, |channel_store, cx| {
|
||||
channel_store.get_channel_member_details(channel_id, cx)
|
||||
})
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker
|
||||
.update(cx, |picker, _| picker.delegate_mut().members = members);
|
||||
})?;
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker.update(cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.mode = mode;
|
||||
delegate.selected_index = 0;
|
||||
picker.set_query("", cx);
|
||||
picker.update_matches(picker.query(cx), cx);
|
||||
cx.notify()
|
||||
});
|
||||
cx.notify()
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate_mut().toggle_selected_member_admin(cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate_mut().remove_selected_member(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ChannelModal {
|
||||
type Event = PickerEvent;
|
||||
}
|
||||
|
||||
impl View for ChannelModal {
|
||||
fn ui_name() -> &'static str {
|
||||
"ChannelModal"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).collab_panel.tabbed_modal;
|
||||
|
||||
let mode = self.picker.read(cx).delegate().mode;
|
||||
let Some(channel) = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.channel_for_id(self.channel_id) else {
|
||||
return Empty::new().into_any()
|
||||
};
|
||||
|
||||
enum InviteMembers {}
|
||||
enum ManageMembers {}
|
||||
|
||||
fn render_mode_button<T: 'static>(
|
||||
mode: Mode,
|
||||
text: &'static str,
|
||||
current_mode: Mode,
|
||||
theme: &theme::TabbedModal,
|
||||
cx: &mut ViewContext<ChannelModal>,
|
||||
) -> AnyElement<ChannelModal> {
|
||||
let active = mode == current_mode;
|
||||
MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
|
||||
let contained_text = theme.tab_button.style_for(active, state);
|
||||
Label::new(text, contained_text.text.clone())
|
||||
.contained()
|
||||
.with_style(contained_text.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if !active {
|
||||
this.set_mode(mode, cx);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(format!("#{}", channel.name), theme.title.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.title.container.clone()),
|
||||
)
|
||||
.with_child(Flex::row().with_children([
|
||||
render_mode_button::<InviteMembers>(
|
||||
Mode::InviteMembers,
|
||||
"Invite members",
|
||||
mode,
|
||||
theme,
|
||||
cx,
|
||||
),
|
||||
render_mode_button::<ManageMembers>(
|
||||
Mode::ManageMembers,
|
||||
"Manage members",
|
||||
mode,
|
||||
theme,
|
||||
cx,
|
||||
),
|
||||
]))
|
||||
.expanded()
|
||||
.contained()
|
||||
.with_style(theme.header),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.picker, cx)
|
||||
.contained()
|
||||
.with_style(theme.body),
|
||||
)
|
||||
.constrained()
|
||||
.with_max_height(theme.max_height)
|
||||
.with_max_width(theme.max_width)
|
||||
.contained()
|
||||
.with_style(theme.modal)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.has_focus = true;
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker)
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Modal for ChannelModal {
|
||||
fn has_focus(&self) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
match event {
|
||||
PickerEvent::Dismiss => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub enum Mode {
|
||||
ManageMembers,
|
||||
InviteMembers,
|
||||
}
|
||||
|
||||
pub struct ChannelModalDelegate {
|
||||
matching_users: Vec<Arc<User>>,
|
||||
matching_member_indices: Vec<usize>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
selected_index: usize,
|
||||
mode: Mode,
|
||||
match_candidates: Vec<StringMatchCandidate>,
|
||||
members: Vec<ChannelMembership>,
|
||||
context_menu: ViewHandle<ContextMenu>,
|
||||
}
|
||||
|
||||
impl PickerDelegate for ChannelModalDelegate {
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search collaborator by username...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.matching_member_indices.len(),
|
||||
Mode::InviteMembers => self.matching_users.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(self.members.iter().enumerate().map(|(id, member)| {
|
||||
StringMatchCandidate {
|
||||
id,
|
||||
string: member.user.github_login.clone(),
|
||||
char_bag: member.user.github_login.chars().collect(),
|
||||
}
|
||||
}));
|
||||
|
||||
let matches = cx.background().block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
cx.background().clone(),
|
||||
));
|
||||
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.matching_member_indices.clear();
|
||||
delegate
|
||||
.matching_member_indices
|
||||
.extend(matches.into_iter().map(|m| m.candidate_id));
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
Mode::InviteMembers => {
|
||||
let search_users = self
|
||||
.user_store
|
||||
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
async {
|
||||
let users = search_users.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.matching_users = users;
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.await;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx),
|
||||
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
|
||||
Some(proto::channel_member::Kind::Invitee) => {
|
||||
self.remove_selected_member(cx);
|
||||
}
|
||||
Some(proto::channel_member::Kind::AncestorMember) | None => {
|
||||
self.invite_member(selected_user, cx)
|
||||
}
|
||||
Some(proto::channel_member::Kind::Member) => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.channel_modal;
|
||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
let (user, admin) = self.user_at_index(ix).unwrap();
|
||||
let request_status = self.member_status(user.id, cx);
|
||||
|
||||
let style = tabbed_modal
|
||||
.picker
|
||||
.item
|
||||
.in_state(selected)
|
||||
.style_for(mouse_state);
|
||||
|
||||
let in_manage = matches!(self.mode, Mode::ManageMembers);
|
||||
|
||||
let mut result = Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(user.github_login.clone(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.contact_username)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_children({
|
||||
(in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|
||||
|| {
|
||||
Label::new("Invited", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left()
|
||||
},
|
||||
)
|
||||
})
|
||||
.with_children(admin.and_then(|admin| {
|
||||
(in_manage && admin).then(|| {
|
||||
Label::new("Admin", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left()
|
||||
})
|
||||
}))
|
||||
.with_children({
|
||||
let svg = match self.mode {
|
||||
Mode::ManageMembers => Some(
|
||||
Svg::new("icons/ellipsis.svg")
|
||||
.with_color(theme.member_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.button_width)
|
||||
.with_height(theme.member_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.member_icon.container),
|
||||
),
|
||||
Mode::InviteMembers => match request_status {
|
||||
Some(proto::channel_member::Kind::Member) => Some(
|
||||
Svg::new("icons/check.svg")
|
||||
.with_color(theme.member_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.button_width)
|
||||
.with_height(theme.member_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.member_icon.container),
|
||||
),
|
||||
Some(proto::channel_member::Kind::Invitee) => Some(
|
||||
Svg::new("icons/check.svg")
|
||||
.with_color(theme.invitee_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.invitee_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.invitee_icon.button_width)
|
||||
.with_height(theme.invitee_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.invitee_icon.container),
|
||||
),
|
||||
Some(proto::channel_member::Kind::AncestorMember) | None => None,
|
||||
},
|
||||
};
|
||||
|
||||
svg.map(|svg| svg.aligned().flex_float().into_any())
|
||||
})
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(tabbed_modal.row_height)
|
||||
.into_any();
|
||||
|
||||
if selected {
|
||||
result = Stack::new()
|
||||
.with_child(result)
|
||||
.with_child(
|
||||
ChildView::new(&self.context_menu, cx)
|
||||
.aligned()
|
||||
.top()
|
||||
.right(),
|
||||
)
|
||||
.into_any();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelModalDelegate {
|
||||
fn member_status(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
cx: &AppContext,
|
||||
) -> Option<proto::channel_member::Kind> {
|
||||
self.members
|
||||
.iter()
|
||||
.find_map(|membership| (membership.user.id == user_id).then_some(membership.kind))
|
||||
.or_else(|| {
|
||||
self.channel_store
|
||||
.read(cx)
|
||||
.has_pending_channel_invite(self.channel_id, user_id)
|
||||
.then_some(proto::channel_member::Kind::Invitee)
|
||||
})
|
||||
}
|
||||
|
||||
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<bool>)> {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
|
||||
let channel_membership = self.members.get(*ix)?;
|
||||
Some((
|
||||
channel_membership.user.clone(),
|
||||
Some(channel_membership.admin),
|
||||
))
|
||||
}),
|
||||
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
let (user, admin) = self.user_at_index(self.selected_index)?;
|
||||
let admin = !admin.unwrap_or(false);
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.set_member_admin(self.channel_id, user.id, admin, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
||||
member.admin = admin;
|
||||
}
|
||||
cx.focus_self();
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
let (user, _) = self.user_at_index(self.selected_index)?;
|
||||
let user_id = user.id;
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.remove_member(self.channel_id, user_id, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
|
||||
this.members.remove(ix);
|
||||
this.matching_member_indices.retain_mut(|member_ix| {
|
||||
if *member_ix == ix {
|
||||
return false;
|
||||
} else if *member_ix > ix {
|
||||
*member_ix -= 1;
|
||||
}
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
this.selected_index = this
|
||||
.selected_index
|
||||
.min(this.matching_member_indices.len().saturating_sub(1));
|
||||
|
||||
cx.focus_self();
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let invite_member = self.channel_store.update(cx, |store, cx| {
|
||||
store.invite_member(self.channel_id, user.id, false, cx)
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
invite_member.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate_mut().members.push(ChannelMembership {
|
||||
user,
|
||||
kind: proto::channel_member::Kind::Invitee,
|
||||
admin: false,
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_menu.update(cx, |context_menu, cx| {
|
||||
context_menu.show(
|
||||
Default::default(),
|
||||
AnchorCorner::TopRight,
|
||||
vec![
|
||||
ContextMenuItem::action("Remove", RemoveMember),
|
||||
ContextMenuItem::action(
|
||||
if user_is_admin {
|
||||
"Make non-admin"
|
||||
} else {
|
||||
"Make admin"
|
||||
},
|
||||
ToggleMemberAdmin,
|
||||
),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,28 +1,132 @@
|
|||
use client::{ContactRequestStatus, User, UserStore};
|
||||
use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
|
||||
use gpui::{
|
||||
elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::sync::Arc;
|
||||
use util::TryFutureExt;
|
||||
use workspace::Modal;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Picker::<ContactFinderDelegate>::init(cx);
|
||||
cx.add_action(ContactFinder::dismiss)
|
||||
}
|
||||
|
||||
pub type ContactFinder = Picker<ContactFinderDelegate>;
|
||||
pub struct ContactFinder {
|
||||
picker: ViewHandle<Picker<ContactFinderDelegate>>,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
pub fn build_contact_finder(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
cx: &mut ViewContext<ContactFinder>,
|
||||
) -> ContactFinder {
|
||||
Picker::new(
|
||||
ContactFinderDelegate {
|
||||
user_store,
|
||||
potential_contacts: Arc::from([]),
|
||||
selected_index: 0,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.contact_finder.picker.clone())
|
||||
impl ContactFinder {
|
||||
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let picker = cx.add_view(|cx| {
|
||||
Picker::new(
|
||||
ContactFinderDelegate {
|
||||
user_store,
|
||||
potential_contacts: Arc::from([]),
|
||||
selected_index: 0,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
|
||||
});
|
||||
|
||||
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
|
||||
|
||||
Self {
|
||||
picker,
|
||||
has_focus: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.set_query(query, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ContactFinder {
|
||||
type Event = PickerEvent;
|
||||
}
|
||||
|
||||
impl View for ContactFinder {
|
||||
fn ui_name() -> &'static str {
|
||||
"ContactFinder"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.tabbed_modal;
|
||||
|
||||
fn render_mode_button(
|
||||
text: &'static str,
|
||||
theme: &theme::TabbedModal,
|
||||
_cx: &mut ViewContext<ContactFinder>,
|
||||
) -> AnyElement<ContactFinder> {
|
||||
let contained_text = &theme.tab_button.active_state().default;
|
||||
Label::new(text, contained_text.text.clone())
|
||||
.contained()
|
||||
.with_style(contained_text.container.clone())
|
||||
.into_any()
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new("Contacts", theme.title.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.title.container.clone()),
|
||||
)
|
||||
.with_child(Flex::row().with_children([render_mode_button(
|
||||
"Invite new contacts",
|
||||
&theme,
|
||||
cx,
|
||||
)]))
|
||||
.expanded()
|
||||
.contained()
|
||||
.with_style(theme.header),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.picker, cx)
|
||||
.contained()
|
||||
.with_style(theme.body),
|
||||
)
|
||||
.constrained()
|
||||
.with_max_height(theme.max_height)
|
||||
.with_max_width(theme.max_width)
|
||||
.contained()
|
||||
.with_style(theme.modal)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.has_focus = true;
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker)
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Modal for ContactFinder {
|
||||
fn has_focus(&self) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
match event {
|
||||
PickerEvent::Dismiss => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContactFinderDelegate {
|
||||
|
@ -97,7 +201,9 @@ impl PickerDelegate for ContactFinderDelegate {
|
|||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let theme = &theme::current(cx);
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.contact_finder;
|
||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
let user = &self.potential_contacts[ix];
|
||||
let request_status = self.user_store.read(cx).contact_request_status(user);
|
||||
|
||||
|
@ -109,12 +215,11 @@ impl PickerDelegate for ContactFinderDelegate {
|
|||
ContactRequestStatus::RequestAccepted => None,
|
||||
};
|
||||
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
|
||||
&theme.contact_finder.disabled_contact_button
|
||||
&theme.disabled_contact_button
|
||||
} else {
|
||||
&theme.contact_finder.contact_button
|
||||
&theme.contact_button
|
||||
};
|
||||
let style = theme
|
||||
.contact_finder
|
||||
let style = tabbed_modal
|
||||
.picker
|
||||
.item
|
||||
.in_state(selected)
|
||||
|
@ -122,14 +227,14 @@ impl PickerDelegate for ContactFinderDelegate {
|
|||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_finder.contact_avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(user.github_login.clone(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.contact_finder.contact_username)
|
||||
.with_style(theme.contact_username)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
|
@ -150,7 +255,7 @@ impl PickerDelegate for ContactFinderDelegate {
|
|||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(theme.contact_finder.row_height)
|
||||
.with_height(tabbed_modal.row_height)
|
||||
.into_any()
|
||||
}
|
||||
}
|
39
crates/collab_ui/src/collab_panel/panel_settings.rs
Normal file
39
crates/collab_ui/src/collab_panel/panel_settings.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use anyhow;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::Setting;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CollaborationPanelDockPosition {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CollaborationPanelSettings {
|
||||
pub button: bool,
|
||||
pub dock: CollaborationPanelDockPosition,
|
||||
pub default_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct CollaborationPanelSettingsContent {
|
||||
pub button: Option<bool>,
|
||||
pub dock: Option<CollaborationPanelDockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
}
|
||||
|
||||
impl Setting for CollaborationPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("collaboration_panel");
|
||||
|
||||
type FileContent = CollaborationPanelSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
|
@ -1,12 +1,10 @@
|
|||
use crate::{
|
||||
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
|
||||
toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
|
||||
ToggleScreenSharing,
|
||||
contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute,
|
||||
toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing,
|
||||
};
|
||||
use call::{ActiveCall, ParticipantLocation, Room};
|
||||
use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
|
||||
use clock::ReplicaId;
|
||||
use contacts_popover::ContactsPopover;
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use gpui::{
|
||||
actions,
|
||||
|
@ -33,7 +31,6 @@ const MAX_BRANCH_NAME_LENGTH: usize = 40;
|
|||
actions!(
|
||||
collab,
|
||||
[
|
||||
ToggleContactsMenu,
|
||||
ToggleUserMenu,
|
||||
ToggleProjectMenu,
|
||||
SwitchBranch,
|
||||
|
@ -43,7 +40,6 @@ actions!(
|
|||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
|
||||
cx.add_action(CollabTitlebarItem::share_project);
|
||||
cx.add_action(CollabTitlebarItem::unshare_project);
|
||||
cx.add_action(CollabTitlebarItem::toggle_user_menu);
|
||||
|
@ -56,7 +52,6 @@ pub struct CollabTitlebarItem {
|
|||
user_store: ModelHandle<UserStore>,
|
||||
client: Arc<Client>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
contacts_popover: Option<ViewHandle<ContactsPopover>>,
|
||||
branch_popover: Option<ViewHandle<BranchList>>,
|
||||
project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
|
||||
user_menu: ViewHandle<ContextMenu>,
|
||||
|
@ -95,7 +90,7 @@ impl View for CollabTitlebarItem {
|
|||
right_container
|
||||
.add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
|
||||
right_container.add_child(self.render_leave_call(&theme, cx));
|
||||
let muted = room.read(cx).is_muted();
|
||||
let muted = room.read(cx).is_muted(cx);
|
||||
let speaking = room.read(cx).is_speaking();
|
||||
left_container.add_child(
|
||||
self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
|
||||
|
@ -109,7 +104,6 @@ impl View for CollabTitlebarItem {
|
|||
let status = workspace.read(cx).client().status();
|
||||
let status = &*status.borrow();
|
||||
if matches!(status, client::Status::Connected { .. }) {
|
||||
right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
|
||||
let avatar = user.as_ref().and_then(|user| user.avatar.clone());
|
||||
right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
|
||||
} else {
|
||||
|
@ -184,7 +178,6 @@ impl CollabTitlebarItem {
|
|||
project,
|
||||
user_store,
|
||||
client,
|
||||
contacts_popover: None,
|
||||
user_menu: cx.add_view(|cx| {
|
||||
let view_id = cx.view_id();
|
||||
let mut menu = ContextMenu::new(view_id, cx);
|
||||
|
@ -315,9 +308,6 @@ impl CollabTitlebarItem {
|
|||
}
|
||||
|
||||
fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if ActiveCall::global(cx).read(cx).room().is_none() {
|
||||
self.contacts_popover = None;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -337,32 +327,6 @@ impl CollabTitlebarItem {
|
|||
.log_err();
|
||||
}
|
||||
|
||||
pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
|
||||
if self.contacts_popover.take().is_none() {
|
||||
let view = cx.add_view(|cx| {
|
||||
ContactsPopover::new(
|
||||
self.project.clone(),
|
||||
self.user_store.clone(),
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&view, |this, _, event, cx| {
|
||||
match event {
|
||||
contacts_popover::Event::Dismissed => {
|
||||
this.contacts_popover = None;
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
self.contacts_popover = Some(view);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
|
||||
self.user_menu.update(cx, |user_menu, cx| {
|
||||
let items = if let Some(_) = self.user_store.read(cx).current_user() {
|
||||
|
@ -390,6 +354,7 @@ impl CollabTitlebarItem {
|
|||
user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_branches_popover_host<'a>(
|
||||
&'a self,
|
||||
_theme: &'a theme::Titlebar,
|
||||
|
@ -403,8 +368,8 @@ impl CollabTitlebarItem {
|
|||
.flex(1., true)
|
||||
.contained()
|
||||
.constrained()
|
||||
.with_width(theme.contacts_popover.width)
|
||||
.with_height(theme.contacts_popover.height)
|
||||
.with_width(theme.titlebar.menu.width)
|
||||
.with_height(theme.titlebar.menu.height)
|
||||
})
|
||||
.on_click(MouseButton::Left, |_, _, _| {})
|
||||
.on_down_out(MouseButton::Left, move |_, this, cx| {
|
||||
|
@ -425,6 +390,7 @@ impl CollabTitlebarItem {
|
|||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
fn render_project_popover_host<'a>(
|
||||
&'a self,
|
||||
_theme: &'a theme::Titlebar,
|
||||
|
@ -438,8 +404,8 @@ impl CollabTitlebarItem {
|
|||
.flex(1., true)
|
||||
.contained()
|
||||
.constrained()
|
||||
.with_width(theme.contacts_popover.width)
|
||||
.with_height(theme.contacts_popover.height)
|
||||
.with_width(theme.titlebar.menu.width)
|
||||
.with_height(theme.titlebar.menu.height)
|
||||
})
|
||||
.on_click(MouseButton::Left, |_, _, _| {})
|
||||
.on_down_out(MouseButton::Left, move |_, this, cx| {
|
||||
|
@ -459,6 +425,7 @@ impl CollabTitlebarItem {
|
|||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
|
||||
if self.branch_popover.take().is_none() {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
|
@ -519,79 +486,7 @@ impl CollabTitlebarItem {
|
|||
}
|
||||
cx.notify();
|
||||
}
|
||||
fn render_toggle_contacts_button(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let titlebar = &theme.titlebar;
|
||||
|
||||
let badge = if self
|
||||
.user_store
|
||||
.read(cx)
|
||||
.incoming_contact_requests()
|
||||
.is_empty()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Empty::new()
|
||||
.collapsed()
|
||||
.contained()
|
||||
.with_style(titlebar.toggle_contacts_badge)
|
||||
.contained()
|
||||
.with_margin_left(
|
||||
titlebar
|
||||
.toggle_contacts_button
|
||||
.inactive_state()
|
||||
.default
|
||||
.icon_width,
|
||||
)
|
||||
.with_margin_top(
|
||||
titlebar
|
||||
.toggle_contacts_button
|
||||
.inactive_state()
|
||||
.default
|
||||
.icon_width,
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
};
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<ToggleContactsMenu, _>(0, cx, |state, _| {
|
||||
let style = titlebar
|
||||
.toggle_contacts_button
|
||||
.in_state(self.contacts_popover.is_some())
|
||||
.style_for(state);
|
||||
Svg::new("icons/radix/person.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.toggle_contacts_popover(&Default::default(), cx)
|
||||
})
|
||||
.with_tooltip::<ToggleContactsMenu>(
|
||||
0,
|
||||
"Show contacts menu",
|
||||
Some(Box::new(ToggleContactsMenu)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
),
|
||||
)
|
||||
.with_children(badge)
|
||||
.with_children(self.render_contacts_popover_host(titlebar, cx))
|
||||
.into_any()
|
||||
}
|
||||
fn render_toggle_screen_sharing_button(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
|
@ -649,7 +544,7 @@ impl CollabTitlebarItem {
|
|||
) -> AnyElement<Self> {
|
||||
let icon;
|
||||
let tooltip;
|
||||
let is_muted = room.read(cx).is_muted();
|
||||
let is_muted = room.read(cx).is_muted(cx);
|
||||
if is_muted {
|
||||
icon = "icons/radix/mic-mute.svg";
|
||||
tooltip = "Unmute microphone";
|
||||
|
@ -923,23 +818,6 @@ impl CollabTitlebarItem {
|
|||
.into_any()
|
||||
}
|
||||
|
||||
fn render_contacts_popover_host<'a>(
|
||||
&'a self,
|
||||
_theme: &'a theme::Titlebar,
|
||||
cx: &'a ViewContext<Self>,
|
||||
) -> Option<AnyElement<Self>> {
|
||||
self.contacts_popover.as_ref().map(|popover| {
|
||||
Overlay::new(ChildView::new(popover, cx))
|
||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
.with_anchor_corner(AnchorCorner::TopLeft)
|
||||
.with_z_index(999)
|
||||
.aligned()
|
||||
.bottom()
|
||||
.right()
|
||||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
fn render_collaborators(
|
||||
&self,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
pub mod collab_panel;
|
||||
mod collab_titlebar_item;
|
||||
mod contact_finder;
|
||||
mod contact_list;
|
||||
mod contact_notification;
|
||||
mod contacts_popover;
|
||||
mod face_pile;
|
||||
mod incoming_call_notification;
|
||||
mod notifications;
|
||||
|
@ -10,7 +8,7 @@ mod project_shared_notification;
|
|||
mod sharing_status_indicator;
|
||||
|
||||
use call::{ActiveCall, Room};
|
||||
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
|
||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||
use gpui::{actions, AppContext, Task};
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
@ -24,9 +22,7 @@ actions!(
|
|||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
vcs_menu::init(cx);
|
||||
collab_titlebar_item::init(cx);
|
||||
contact_list::init(cx);
|
||||
contact_finder::init(cx);
|
||||
contacts_popover::init(cx);
|
||||
collab_panel::init(app_state.client.clone(), cx);
|
||||
incoming_call_notification::init(&app_state, cx);
|
||||
project_shared_notification::init(&app_state, cx);
|
||||
sharing_status_indicator::init(cx);
|
||||
|
@ -68,7 +64,7 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
|
|||
if let Some(room) = call.room().cloned() {
|
||||
let client = call.client();
|
||||
room.update(cx, |room, cx| {
|
||||
if room.is_muted() {
|
||||
if room.is_muted(cx) {
|
||||
ActiveCall::report_call_event_for_room("enable microphone", room.id(), &client, cx);
|
||||
} else {
|
||||
ActiveCall::report_call_event_for_room(
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,137 +0,0 @@
|
|||
use crate::{
|
||||
contact_finder::{build_contact_finder, ContactFinder},
|
||||
contact_list::ContactList,
|
||||
};
|
||||
use client::UserStore;
|
||||
use gpui::{
|
||||
actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use picker::PickerEvent;
|
||||
use project::Project;
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(contacts_popover, [ToggleContactFinder]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(ContactsPopover::toggle_contact_finder);
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
enum Child {
|
||||
ContactList(ViewHandle<ContactList>),
|
||||
ContactFinder(ViewHandle<ContactFinder>),
|
||||
}
|
||||
|
||||
pub struct ContactsPopover {
|
||||
child: Child,
|
||||
project: ModelHandle<Project>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
_subscription: Option<gpui::Subscription>,
|
||||
}
|
||||
|
||||
impl ContactsPopover {
|
||||
pub fn new(
|
||||
project: ModelHandle<Project>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let mut this = Self {
|
||||
child: Child::ContactList(cx.add_view(|cx| {
|
||||
ContactList::new(project.clone(), user_store.clone(), workspace.clone(), cx)
|
||||
})),
|
||||
project,
|
||||
user_store,
|
||||
workspace,
|
||||
_subscription: None,
|
||||
};
|
||||
this.show_contact_list(String::new(), cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
|
||||
match &self.child {
|
||||
Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx),
|
||||
Child::ContactFinder(finder) => self.show_contact_list(finder.read(cx).query(cx), cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
|
||||
let child = cx.add_view(|cx| {
|
||||
let finder = build_contact_finder(self.user_store.clone(), cx);
|
||||
finder.set_query(editor_text, cx);
|
||||
finder
|
||||
});
|
||||
cx.focus(&child);
|
||||
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
||||
PickerEvent::Dismiss => cx.emit(Event::Dismissed),
|
||||
}));
|
||||
self.child = Child::ContactFinder(child);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
|
||||
let child = cx.add_view(|cx| {
|
||||
ContactList::new(
|
||||
self.project.clone(),
|
||||
self.user_store.clone(),
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
.with_editor_text(editor_text, cx)
|
||||
});
|
||||
cx.focus(&child);
|
||||
self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event {
|
||||
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||
crate::contact_list::Event::ToggleContactFinder => {
|
||||
this.toggle_contact_finder(&Default::default(), cx)
|
||||
}
|
||||
}));
|
||||
self.child = Child::ContactList(child);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ContactsPopover {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for ContactsPopover {
|
||||
fn ui_name() -> &'static str {
|
||||
"ContactsPopover"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = theme::current(cx).clone();
|
||||
let child = match &self.child {
|
||||
Child::ContactList(child) => ChildView::new(child, cx),
|
||||
Child::ContactFinder(child) => ChildView::new(child, cx),
|
||||
};
|
||||
|
||||
MouseEventHandler::new::<ContactsPopover, _>(0, cx, |_, _| {
|
||||
Flex::column()
|
||||
.with_child(child.flex(1., true))
|
||||
.contained()
|
||||
.with_style(theme.contacts_popover.container)
|
||||
.constrained()
|
||||
.with_width(theme.contacts_popover.width)
|
||||
.with_height(theme.contacts_popover.height)
|
||||
})
|
||||
.on_down_out(MouseButton::Left, move |_, _, cx| cx.emit(Event::Dismissed))
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
match &self.child {
|
||||
Child::ContactList(child) => cx.focus(child),
|
||||
Child::ContactFinder(child) => cx.focus(child),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,44 +7,48 @@ use gpui::{
|
|||
},
|
||||
json::ToJson,
|
||||
serde_json::{self, json},
|
||||
AnyElement, Axis, Element, LayoutContext, PaintContext, SceneBuilder, ViewContext,
|
||||
AnyElement, Axis, Element, LayoutContext, PaintContext, SceneBuilder, View, ViewContext,
|
||||
};
|
||||
|
||||
use crate::CollabTitlebarItem;
|
||||
|
||||
pub(crate) struct FacePile {
|
||||
pub(crate) struct FacePile<V: View> {
|
||||
overlap: f32,
|
||||
faces: Vec<AnyElement<CollabTitlebarItem>>,
|
||||
faces: Vec<AnyElement<V>>,
|
||||
}
|
||||
|
||||
impl FacePile {
|
||||
pub fn new(overlap: f32) -> FacePile {
|
||||
FacePile {
|
||||
impl<V: View> FacePile<V> {
|
||||
pub fn new(overlap: f32) -> Self {
|
||||
Self {
|
||||
overlap,
|
||||
faces: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element<CollabTitlebarItem> for FacePile {
|
||||
impl<V: View> Element<V> for FacePile<V> {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: gpui::SizeConstraint,
|
||||
view: &mut CollabTitlebarItem,
|
||||
cx: &mut LayoutContext<CollabTitlebarItem>,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
|
||||
|
||||
let mut width = 0.;
|
||||
let mut max_height = 0.;
|
||||
for face in &mut self.faces {
|
||||
width += face.layout(constraint, view, cx).x();
|
||||
let layout = face.layout(constraint, view, cx);
|
||||
width += layout.x();
|
||||
max_height = f32::max(max_height, layout.y());
|
||||
}
|
||||
width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
|
||||
|
||||
(Vector2F::new(width, constraint.max.y()), ())
|
||||
(
|
||||
Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
|
||||
(),
|
||||
)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
|
@ -53,8 +57,8 @@ impl Element<CollabTitlebarItem> for FacePile {
|
|||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
_layout: &mut Self::LayoutState,
|
||||
view: &mut CollabTitlebarItem,
|
||||
cx: &mut PaintContext<CollabTitlebarItem>,
|
||||
view: &mut V,
|
||||
cx: &mut PaintContext<V>,
|
||||
) -> Self::PaintState {
|
||||
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||
|
||||
|
@ -64,6 +68,7 @@ impl Element<CollabTitlebarItem> for FacePile {
|
|||
for face in self.faces.iter_mut().rev() {
|
||||
let size = face.size();
|
||||
origin_x -= size.x();
|
||||
let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
|
||||
scene.paint_layer(None, |scene| {
|
||||
face.paint(scene, vec2f(origin_x, origin_y), visible_bounds, view, cx);
|
||||
});
|
||||
|
@ -80,8 +85,8 @@ impl Element<CollabTitlebarItem> for FacePile {
|
|||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &CollabTitlebarItem,
|
||||
_: &ViewContext<CollabTitlebarItem>,
|
||||
_: &V,
|
||||
_: &ViewContext<V>,
|
||||
) -> Option<RectF> {
|
||||
None
|
||||
}
|
||||
|
@ -91,8 +96,8 @@ impl Element<CollabTitlebarItem> for FacePile {
|
|||
bounds: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &CollabTitlebarItem,
|
||||
_: &ViewContext<CollabTitlebarItem>,
|
||||
_: &V,
|
||||
_: &ViewContext<V>,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "FacePile",
|
||||
|
@ -101,8 +106,8 @@ impl Element<CollabTitlebarItem> for FacePile {
|
|||
}
|
||||
}
|
||||
|
||||
impl Extend<AnyElement<CollabTitlebarItem>> for FacePile {
|
||||
fn extend<T: IntoIterator<Item = AnyElement<CollabTitlebarItem>>>(&mut self, children: T) {
|
||||
impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
|
||||
fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
|
||||
self.faces.extend(children);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ impl View for DiagnosticIndicator {
|
|||
let mut summary_row = Flex::row();
|
||||
if self.summary.error_count > 0 {
|
||||
summary_row.add_child(
|
||||
Svg::new("icons/circle_x_mark_16.svg")
|
||||
Svg::new("icons/error.svg")
|
||||
.with_color(style.icon_color_error)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
|
@ -121,7 +121,7 @@ impl View for DiagnosticIndicator {
|
|||
|
||||
if self.summary.warning_count > 0 {
|
||||
summary_row.add_child(
|
||||
Svg::new("icons/triangle_exclamation_16.svg")
|
||||
Svg::new("icons/warning.svg")
|
||||
.with_color(style.icon_color_warning)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
|
@ -142,7 +142,7 @@ impl View for DiagnosticIndicator {
|
|||
|
||||
if self.summary.error_count == 0 && self.summary.warning_count == 0 {
|
||||
summary_row.add_child(
|
||||
Svg::new("icons/circle_check_16.svg")
|
||||
Svg::new("icons/check_circle.svg")
|
||||
.with_color(style.icon_color_ok)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
|
|
|
@ -302,10 +302,11 @@ actions!(
|
|||
Hover,
|
||||
Format,
|
||||
ToggleSoftWrap,
|
||||
ToggleInlayHints,
|
||||
RevealInFinder,
|
||||
CopyPath,
|
||||
CopyRelativePath,
|
||||
CopyHighlightJson
|
||||
CopyHighlightJson,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -446,6 +447,7 @@ pub fn init(cx: &mut AppContext) {
|
|||
cx.add_action(Editor::toggle_code_actions);
|
||||
cx.add_action(Editor::open_excerpts);
|
||||
cx.add_action(Editor::toggle_soft_wrap);
|
||||
cx.add_action(Editor::toggle_inlay_hints);
|
||||
cx.add_action(Editor::reveal_in_finder);
|
||||
cx.add_action(Editor::copy_path);
|
||||
cx.add_action(Editor::copy_relative_path);
|
||||
|
@ -1237,7 +1239,8 @@ enum GotoDefinitionKind {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum InlayRefreshReason {
|
||||
enum InlayHintRefreshReason {
|
||||
Toggle(bool),
|
||||
SettingsChange(InlayHintSettings),
|
||||
NewLinesShown,
|
||||
BufferEdited(HashSet<Arc<Language>>),
|
||||
|
@ -1354,8 +1357,8 @@ impl Editor {
|
|||
}));
|
||||
}
|
||||
project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
|
||||
if let project::Event::RefreshInlays = event {
|
||||
editor.refresh_inlays(InlayRefreshReason::RefreshRequested, cx);
|
||||
if let project::Event::RefreshInlayHints = event {
|
||||
editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
@ -2669,13 +2672,41 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext<Self>) {
|
||||
pub fn toggle_inlay_hints(&mut self, _: &ToggleInlayHints, cx: &mut ViewContext<Self>) {
|
||||
self.refresh_inlay_hints(
|
||||
InlayHintRefreshReason::Toggle(!self.inlay_hint_cache.enabled),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn inlay_hints_enabled(&self) -> bool {
|
||||
self.inlay_hint_cache.enabled
|
||||
}
|
||||
|
||||
fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut ViewContext<Self>) {
|
||||
if self.project.is_none() || self.mode != EditorMode::Full {
|
||||
return;
|
||||
}
|
||||
|
||||
let (invalidate_cache, required_languages) = match reason {
|
||||
InlayRefreshReason::SettingsChange(new_settings) => {
|
||||
InlayHintRefreshReason::Toggle(enabled) => {
|
||||
self.inlay_hint_cache.enabled = enabled;
|
||||
if enabled {
|
||||
(InvalidationStrategy::RefreshRequested, None)
|
||||
} else {
|
||||
self.inlay_hint_cache.clear();
|
||||
self.splice_inlay_hints(
|
||||
self.visible_inlay_hints(cx)
|
||||
.iter()
|
||||
.map(|inlay| inlay.id)
|
||||
.collect(),
|
||||
Vec::new(),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
InlayHintRefreshReason::SettingsChange(new_settings) => {
|
||||
match self.inlay_hint_cache.update_settings(
|
||||
&self.buffer,
|
||||
new_settings,
|
||||
|
@ -2693,11 +2724,13 @@ impl Editor {
|
|||
ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
|
||||
}
|
||||
}
|
||||
InlayRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
|
||||
InlayRefreshReason::BufferEdited(buffer_languages) => {
|
||||
InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
|
||||
InlayHintRefreshReason::BufferEdited(buffer_languages) => {
|
||||
(InvalidationStrategy::BufferEdited, Some(buffer_languages))
|
||||
}
|
||||
InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None),
|
||||
InlayHintRefreshReason::RefreshRequested => {
|
||||
(InvalidationStrategy::RefreshRequested, None)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(InlaySplice {
|
||||
|
@ -2774,6 +2807,7 @@ impl Editor {
|
|||
self.display_map.update(cx, |display_map, cx| {
|
||||
display_map.splice_inlays(to_remove, to_insert, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn trigger_on_type_formatting(
|
||||
|
@ -7696,8 +7730,8 @@ impl Editor {
|
|||
.cloned()
|
||||
.collect::<HashSet<_>>();
|
||||
if !languages_affected.is_empty() {
|
||||
self.refresh_inlays(
|
||||
InlayRefreshReason::BufferEdited(languages_affected),
|
||||
self.refresh_inlay_hints(
|
||||
InlayHintRefreshReason::BufferEdited(languages_affected),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
@ -7735,8 +7769,8 @@ impl Editor {
|
|||
|
||||
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.refresh_copilot_suggestions(true, cx);
|
||||
self.refresh_inlays(
|
||||
InlayRefreshReason::SettingsChange(inlay_hint_settings(
|
||||
self.refresh_inlay_hints(
|
||||
InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
|
||||
self.selections.newest_anchor().head(),
|
||||
&self.buffer.read(cx).snapshot(cx),
|
||||
cx,
|
||||
|
|
|
@ -24,7 +24,7 @@ pub struct InlayHintCache {
|
|||
hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
|
||||
allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
|
||||
version: usize,
|
||||
enabled: bool,
|
||||
pub(super) enabled: bool,
|
||||
update_tasks: HashMap<ExcerptId, TasksForRanges>,
|
||||
}
|
||||
|
||||
|
@ -380,7 +380,7 @@ impl InlayHintCache {
|
|||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
pub fn clear(&mut self) {
|
||||
self.version += 1;
|
||||
self.update_tasks.clear();
|
||||
self.hints.clear();
|
||||
|
@ -2001,7 +2001,7 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_multiple_excerpts_large_multibuffer(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
|
@ -2335,10 +2335,12 @@ mod tests {
|
|||
all hints should be invalidated and requeried for all of its visible excerpts"
|
||||
);
|
||||
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
||||
assert_eq!(
|
||||
editor.inlay_hint_cache().version,
|
||||
last_scroll_update_version + expected_layers.len(),
|
||||
"Due to every excerpt having one hint, cache should update per new excerpt received"
|
||||
|
||||
let current_cache_version = editor.inlay_hint_cache().version;
|
||||
let minimum_expected_version = last_scroll_update_version + expected_layers.len();
|
||||
assert!(
|
||||
current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1,
|
||||
"Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -2683,6 +2685,127 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
enabled: false,
|
||||
show_type_hints: true,
|
||||
show_parameter_hints: true,
|
||||
show_other_hints: true,
|
||||
})
|
||||
});
|
||||
|
||||
let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
|
||||
});
|
||||
cx.foreground().start_waiting();
|
||||
let lsp_request_count = Arc::new(AtomicU32::new(0));
|
||||
let closure_lsp_request_count = Arc::clone(&lsp_request_count);
|
||||
fake_server
|
||||
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
|
||||
let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
|
||||
async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(file_with_hints).unwrap(),
|
||||
);
|
||||
|
||||
let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
|
||||
Ok(Some(vec![lsp::InlayHint {
|
||||
position: lsp::Position::new(0, i),
|
||||
label: lsp::InlayHintLabel::String(i.to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
}]))
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
cx.foreground().run_until_parked();
|
||||
editor.update(cx, |editor, cx| {
|
||||
let expected_hints = vec!["1".to_string()];
|
||||
assert_eq!(
|
||||
expected_hints,
|
||||
cached_hint_labels(editor),
|
||||
"Should display inlays after toggle despite them disabled in settings"
|
||||
);
|
||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||
assert_eq!(
|
||||
editor.inlay_hint_cache().version,
|
||||
1,
|
||||
"First toggle should be cache's first update"
|
||||
);
|
||||
});
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert!(
|
||||
cached_hint_labels(editor).is_empty(),
|
||||
"Should clear hints after 2nd toggle"
|
||||
);
|
||||
assert!(visible_hint_labels(editor, cx).is_empty());
|
||||
assert_eq!(editor.inlay_hint_cache().version, 2);
|
||||
});
|
||||
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
enabled: true,
|
||||
show_type_hints: true,
|
||||
show_parameter_hints: true,
|
||||
show_other_hints: true,
|
||||
})
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
editor.update(cx, |editor, cx| {
|
||||
let expected_hints = vec!["2".to_string()];
|
||||
assert_eq!(
|
||||
expected_hints,
|
||||
cached_hint_labels(editor),
|
||||
"Should query LSP hints for the 2nd time after enabling hints in settings"
|
||||
);
|
||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||
assert_eq!(editor.inlay_hint_cache().version, 3);
|
||||
});
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert!(
|
||||
cached_hint_labels(editor).is_empty(),
|
||||
"Should clear hints after enabling in settings and a 3rd toggle"
|
||||
);
|
||||
assert!(visible_hint_labels(editor, cx).is_empty());
|
||||
assert_eq!(editor.inlay_hint_cache().version, 4);
|
||||
});
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
editor.update(cx, |editor, cx| {
|
||||
let expected_hints = vec!["3".to_string()];
|
||||
assert_eq!(
|
||||
expected_hints,
|
||||
cached_hint_labels(editor),
|
||||
"Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
|
||||
);
|
||||
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
|
||||
assert_eq!(editor.inlay_hint_cache().version, 5);
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
|
@ -2759,6 +2882,12 @@ all hints should be invalidated and requeried for all of its visible excerpts"
|
|||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert!(cached_hint_labels(editor).is_empty());
|
||||
assert!(visible_hint_labels(editor, cx).is_empty());
|
||||
assert_eq!(editor.inlay_hint_cache().version, 0);
|
||||
});
|
||||
|
||||
("/a/main.rs", editor, fake_server)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ use crate::{
|
|||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
hover_popover::hide_hover,
|
||||
persistence::DB,
|
||||
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayRefreshReason, MultiBufferSnapshot,
|
||||
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot,
|
||||
ToPoint,
|
||||
};
|
||||
|
||||
|
@ -301,7 +301,7 @@ impl Editor {
|
|||
cx.spawn(|editor, mut cx| async move {
|
||||
editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
editor.refresh_inlays(InlayRefreshReason::NewLinesShown, cx)
|
||||
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
|
@ -333,7 +333,7 @@ impl Editor {
|
|||
cx,
|
||||
);
|
||||
|
||||
self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx);
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
|
||||
|
|
|
@ -44,7 +44,7 @@ impl View for DeployFeedbackButton {
|
|||
.in_state(active)
|
||||
.style_for(state);
|
||||
|
||||
Svg::new("icons/feedback_16.svg")
|
||||
Svg::new("icons/feedback.svg")
|
||||
.with_color(style.icon_color)
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
|
|
|
@ -577,6 +577,14 @@ impl AppContext {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn optional_global<T: 'static>(&self) -> Option<&T> {
|
||||
if let Some(global) = self.globals.get(&TypeId::of::<T>()) {
|
||||
Some(global.downcast_ref().unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn upgrade(&self) -> App {
|
||||
App(self.weak_self.as_ref().unwrap().upgrade().unwrap())
|
||||
}
|
||||
|
|
|
@ -48,6 +48,10 @@ pub trait Element<V: View>: 'static {
|
|||
type LayoutState;
|
||||
type PaintState;
|
||||
|
||||
fn view_name(&self) -> &'static str {
|
||||
V::ui_name()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
|
@ -182,16 +186,27 @@ pub trait Element<V: View>: 'static {
|
|||
Tooltip::new::<Tag>(id, text, action, style, self.into_any(), cx)
|
||||
}
|
||||
|
||||
fn resizable(
|
||||
/// Uses the the given element to calculate resizes for the given tag
|
||||
fn provide_resize_bounds<Tag: 'static>(self) -> BoundsProvider<V, Tag>
|
||||
where
|
||||
Self: 'static + Sized,
|
||||
{
|
||||
BoundsProvider::<_, Tag>::new(self.into_any())
|
||||
}
|
||||
|
||||
/// Calls the given closure with the new size of the element whenever the
|
||||
/// handle is dragged. This will be calculated in relation to the bounds
|
||||
/// provided by the given tag
|
||||
fn resizable<Tag: 'static>(
|
||||
self,
|
||||
side: HandleSide,
|
||||
size: f32,
|
||||
on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
|
||||
on_resize: impl 'static + FnMut(&mut V, Option<f32>, &mut ViewContext<V>),
|
||||
) -> Resizable<V>
|
||||
where
|
||||
Self: 'static + Sized,
|
||||
{
|
||||
Resizable::new(self.into_any(), side, size, on_resize)
|
||||
Resizable::new::<Tag>(self.into_any(), side, size, on_resize)
|
||||
}
|
||||
|
||||
fn mouse<Tag: 'static>(self, region_id: usize) -> MouseEventHandler<V>
|
||||
|
@ -272,8 +287,16 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
|
|||
| ElementState::PostLayout { mut element, .. }
|
||||
| ElementState::PostPaint { mut element, .. } => {
|
||||
let (size, layout) = element.layout(constraint, view, cx);
|
||||
debug_assert!(size.x().is_finite());
|
||||
debug_assert!(size.y().is_finite());
|
||||
debug_assert!(
|
||||
size.x().is_finite(),
|
||||
"Element for {:?} had infinite x size after layout",
|
||||
element.view_name()
|
||||
);
|
||||
debug_assert!(
|
||||
size.y().is_finite(),
|
||||
"Element for {:?} had infinite y size after layout",
|
||||
element.view_name()
|
||||
);
|
||||
|
||||
result = size;
|
||||
ElementState::PostLayout {
|
||||
|
|
|
@ -82,6 +82,9 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
|
|||
view: &V,
|
||||
cx: &ViewContext<V>,
|
||||
) -> serde_json::Value {
|
||||
element.debug(view, cx)
|
||||
serde_json::json!({
|
||||
"type": "ComponentAdapter",
|
||||
"child": element.debug(view, cx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use collections::HashMap;
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
geometry::rect::RectF,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
scene::MouseDrag,
|
||||
AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
|
||||
SizeConstraint, View, ViewContext,
|
||||
AnyElement, AppContext, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
|
||||
SizeConstraint, TypeTag, View, ViewContext,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
|
@ -27,15 +27,6 @@ impl HandleSide {
|
|||
}
|
||||
}
|
||||
|
||||
/// 'before' is in reference to the standard english document ordering of left-to-right
|
||||
/// then top-to-bottom
|
||||
fn before_content(self) -> bool {
|
||||
match self {
|
||||
HandleSide::Left | HandleSide::Top => true,
|
||||
HandleSide::Right | HandleSide::Bottom => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn relevant_component(&self, vector: Vector2F) -> f32 {
|
||||
match self.axis() {
|
||||
Axis::Horizontal => vector.x(),
|
||||
|
@ -43,14 +34,6 @@ impl HandleSide {
|
|||
}
|
||||
}
|
||||
|
||||
fn compute_delta(&self, e: MouseDrag) -> f32 {
|
||||
if self.before_content() {
|
||||
self.relevant_component(e.prev_mouse_position) - self.relevant_component(e.position)
|
||||
} else {
|
||||
self.relevant_component(e.position) - self.relevant_component(e.prev_mouse_position)
|
||||
}
|
||||
}
|
||||
|
||||
fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
|
||||
match self {
|
||||
HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
|
||||
|
@ -69,21 +52,29 @@ impl HandleSide {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_bounds(tag: TypeTag, cx: &AppContext) -> Option<&(RectF, RectF)>
|
||||
where
|
||||
{
|
||||
cx.optional_global::<ProviderMap>()
|
||||
.and_then(|map| map.0.get(&tag))
|
||||
}
|
||||
|
||||
pub struct Resizable<V: View> {
|
||||
child: AnyElement<V>,
|
||||
tag: TypeTag,
|
||||
handle_side: HandleSide,
|
||||
handle_size: f32,
|
||||
on_resize: Rc<RefCell<dyn FnMut(&mut V, f32, &mut ViewContext<V>)>>,
|
||||
on_resize: Rc<RefCell<dyn FnMut(&mut V, Option<f32>, &mut ViewContext<V>)>>,
|
||||
}
|
||||
|
||||
const DEFAULT_HANDLE_SIZE: f32 = 4.0;
|
||||
|
||||
impl<V: View> Resizable<V> {
|
||||
pub fn new(
|
||||
pub fn new<Tag: 'static>(
|
||||
child: AnyElement<V>,
|
||||
handle_side: HandleSide,
|
||||
size: f32,
|
||||
on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
|
||||
on_resize: impl 'static + FnMut(&mut V, Option<f32>, &mut ViewContext<V>),
|
||||
) -> Self {
|
||||
let child = match handle_side.axis() {
|
||||
Axis::Horizontal => child.constrained().with_max_width(size),
|
||||
|
@ -94,6 +85,7 @@ impl<V: View> Resizable<V> {
|
|||
Self {
|
||||
child,
|
||||
handle_side,
|
||||
tag: TypeTag::new::<Tag>(),
|
||||
handle_size: DEFAULT_HANDLE_SIZE,
|
||||
on_resize: Rc::new(RefCell::new(on_resize)),
|
||||
}
|
||||
|
@ -139,6 +131,14 @@ impl<V: View> Element<V> for Resizable<V> {
|
|||
handle_region,
|
||||
)
|
||||
.on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
|
||||
.on_click(MouseButton::Left, {
|
||||
let on_resize = self.on_resize.clone();
|
||||
move |click, v, cx| {
|
||||
if click.click_count == 2 {
|
||||
on_resize.borrow_mut()(v, None, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_drag(MouseButton::Left, {
|
||||
let bounds = bounds.clone();
|
||||
let side = self.handle_side;
|
||||
|
@ -146,16 +146,30 @@ impl<V: View> Element<V> for Resizable<V> {
|
|||
let min_size = side.relevant_component(constraint.min);
|
||||
let max_size = side.relevant_component(constraint.max);
|
||||
let on_resize = self.on_resize.clone();
|
||||
let tag = self.tag;
|
||||
move |event, view: &mut V, cx| {
|
||||
if event.end {
|
||||
return;
|
||||
}
|
||||
let new_size = min_size
|
||||
.max(prev_size + side.compute_delta(event))
|
||||
.min(max_size)
|
||||
.round();
|
||||
|
||||
let Some((bounds, _)) = get_bounds(tag, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let new_size_raw = match side {
|
||||
// Handle on top side of element => Element is on bottom
|
||||
HandleSide::Top => bounds.height() + bounds.origin_y() - event.position.y(),
|
||||
// Handle on right side of element => Element is on left
|
||||
HandleSide::Right => event.position.x() - bounds.lower_left().x(),
|
||||
// Handle on left side of element => Element is on the right
|
||||
HandleSide::Left => bounds.width() + bounds.origin_x() - event.position.x(),
|
||||
// Handle on bottom side of element => Element is on the top
|
||||
HandleSide::Bottom => event.position.y() - bounds.lower_left().y(),
|
||||
};
|
||||
|
||||
let new_size = min_size.max(new_size_raw).min(max_size).round();
|
||||
if new_size != prev_size {
|
||||
on_resize.borrow_mut()(view, new_size, cx);
|
||||
on_resize.borrow_mut()(view, Some(new_size), cx);
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
@ -201,3 +215,80 @@ impl<V: View> Element<V> for Resizable<V> {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ProviderMap(HashMap<TypeTag, (RectF, RectF)>);
|
||||
|
||||
pub struct BoundsProvider<V: View, P> {
|
||||
child: AnyElement<V>,
|
||||
phantom: std::marker::PhantomData<P>,
|
||||
}
|
||||
|
||||
impl<V: View, P: 'static> BoundsProvider<V, P> {
|
||||
pub fn new(child: AnyElement<V>) -> Self {
|
||||
Self {
|
||||
child,
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View, P: 'static> Element<V> for BoundsProvider<V, P> {
|
||||
type LayoutState = ();
|
||||
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: crate::SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut crate::LayoutContext<V>,
|
||||
) -> (pathfinder_geometry::vector::Vector2F, Self::LayoutState) {
|
||||
(self.child.layout(constraint, view, cx), ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
scene: &mut crate::SceneBuilder,
|
||||
bounds: pathfinder_geometry::rect::RectF,
|
||||
visible_bounds: pathfinder_geometry::rect::RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
view: &mut V,
|
||||
cx: &mut crate::PaintContext<V>,
|
||||
) -> Self::PaintState {
|
||||
cx.update_default_global::<ProviderMap, _, _>(|map, _| {
|
||||
map.0.insert(TypeTag::new::<P>(), (bounds, visible_bounds));
|
||||
});
|
||||
|
||||
self.child
|
||||
.paint(scene, bounds.origin(), visible_bounds, view, cx)
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: std::ops::Range<usize>,
|
||||
_: pathfinder_geometry::rect::RectF,
|
||||
_: pathfinder_geometry::rect::RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
view: &V,
|
||||
cx: &crate::ViewContext<V>,
|
||||
) -> Option<pathfinder_geometry::rect::RectF> {
|
||||
self.child.rect_for_text_range(range_utf16, view, cx)
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
_: pathfinder_geometry::rect::RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
view: &V,
|
||||
cx: &crate::ViewContext<V>,
|
||||
) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "Provider",
|
||||
"providing": format!("{:?}", TypeTag::new::<P>()),
|
||||
"child": self.child.debug(view, cx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ gpui::actions!(
|
|||
SelectPrev,
|
||||
SelectNext,
|
||||
SelectFirst,
|
||||
SelectLast
|
||||
SelectLast,
|
||||
ShowContextMenu
|
||||
]
|
||||
);
|
||||
|
|
|
@ -13,6 +13,7 @@ use std::{cmp, sync::Arc};
|
|||
use util::ResultExt;
|
||||
use workspace::Modal;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum PickerEvent {
|
||||
Dismiss,
|
||||
}
|
||||
|
|
|
@ -282,7 +282,7 @@ pub enum Event {
|
|||
new_peer_id: proto::PeerId,
|
||||
},
|
||||
CollaboratorLeft(proto::PeerId),
|
||||
RefreshInlays,
|
||||
RefreshInlayHints,
|
||||
}
|
||||
|
||||
pub enum LanguageServerState {
|
||||
|
@ -2872,7 +2872,7 @@ impl Project {
|
|||
.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("project dropped"))?;
|
||||
this.update(&mut cx, |project, cx| {
|
||||
cx.emit(Event::RefreshInlays);
|
||||
cx.emit(Event::RefreshInlayHints);
|
||||
project.remote_id().map(|project_id| {
|
||||
project.client.send(proto::RefreshInlayHints { project_id })
|
||||
})
|
||||
|
@ -3436,7 +3436,7 @@ impl Project {
|
|||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
|
||||
cx.emit(Event::RefreshInlays);
|
||||
cx.emit(Event::RefreshInlayHints);
|
||||
status.pending_work.remove(&token);
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -6810,7 +6810,7 @@ impl Project {
|
|||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::Ack> {
|
||||
this.update(&mut cx, |_, cx| {
|
||||
cx.emit(Event::RefreshInlays);
|
||||
cx.emit(Event::RefreshInlayHints);
|
||||
});
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
|
|
@ -1651,30 +1651,14 @@ impl workspace::dock::Panel for ProjectPanel {
|
|||
.unwrap_or_else(|| settings::get::<ProjectPanelSettings>(cx).default_width)
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
|
||||
self.width = Some(size);
|
||||
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
|
||||
self.width = size;
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn should_zoom_in_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn should_zoom_out_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_zoomed(&self, _: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_zoomed(&mut self, _: bool, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn set_active(&mut self, _: bool, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn icon_path(&self) -> &'static str {
|
||||
"icons/folder_tree_16.svg"
|
||||
fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
|
||||
Some("icons/project.svg")
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
|
||||
|
@ -1685,14 +1669,6 @@ impl workspace::dock::Panel for ProjectPanel {
|
|||
matches!(event, Event::DockPositionChanged)
|
||||
}
|
||||
|
||||
fn should_activate_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn should_close_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn has_focus(&self, _: &WindowContext) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
|
22
crates/quick_action_bar/Cargo.toml
Normal file
22
crates/quick_action_bar/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "quick_action_bar"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/quick_action_bar.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
search = { path = "../search" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
theme = { path = "../theme", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
163
crates/quick_action_bar/src/quick_action_bar.rs
Normal file
163
crates/quick_action_bar/src/quick_action_bar.rs
Normal file
|
@ -0,0 +1,163 @@
|
|||
use editor::Editor;
|
||||
use gpui::{
|
||||
elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
use search::{buffer_search, BufferSearchBar};
|
||||
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
pub struct QuickActionBar {
|
||||
buffer_search_bar: ViewHandle<BufferSearchBar>,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
_inlay_hints_enabled_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl QuickActionBar {
|
||||
pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>) -> Self {
|
||||
Self {
|
||||
buffer_search_bar,
|
||||
active_item: None,
|
||||
_inlay_hints_enabled_subscription: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn active_editor(&self) -> Option<ViewHandle<Editor>> {
|
||||
self.active_item
|
||||
.as_ref()
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for QuickActionBar {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for QuickActionBar {
|
||||
fn ui_name() -> &'static str {
|
||||
"QuickActionsBar"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
|
||||
let Some(editor) = self.active_editor() else { return Empty::new().into_any(); };
|
||||
|
||||
let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
|
||||
let mut bar = Flex::row().with_child(render_quick_action_bar_button(
|
||||
0,
|
||||
"icons/inlay_hint.svg",
|
||||
inlay_hints_enabled,
|
||||
(
|
||||
"Toggle Inlay Hints".to_string(),
|
||||
Some(Box::new(editor::ToggleInlayHints)),
|
||||
),
|
||||
cx,
|
||||
|this, cx| {
|
||||
if let Some(editor) = this.active_editor() {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
if editor.read(cx).buffer().read(cx).is_singleton() {
|
||||
let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed();
|
||||
let search_action = buffer_search::Deploy { focus: true };
|
||||
|
||||
bar = bar.with_child(render_quick_action_bar_button(
|
||||
1,
|
||||
"icons/magnifying_glass.svg",
|
||||
search_bar_shown,
|
||||
(
|
||||
"Buffer Search".to_string(),
|
||||
Some(Box::new(search_action.clone())),
|
||||
),
|
||||
cx,
|
||||
move |this, cx| {
|
||||
this.buffer_search_bar.update(cx, |buffer_search_bar, cx| {
|
||||
if search_bar_shown {
|
||||
buffer_search_bar.dismiss(&buffer_search::Dismiss, cx);
|
||||
} else {
|
||||
buffer_search_bar.deploy(&search_action, cx);
|
||||
}
|
||||
});
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
bar.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_quick_action_bar_button<
|
||||
F: 'static + Fn(&mut QuickActionBar, &mut EventContext<QuickActionBar>),
|
||||
>(
|
||||
index: usize,
|
||||
icon: &'static str,
|
||||
toggled: bool,
|
||||
tooltip: (String, Option<Box<dyn Action>>),
|
||||
cx: &mut ViewContext<QuickActionBar>,
|
||||
on_click: F,
|
||||
) -> AnyElement<QuickActionBar> {
|
||||
enum QuickActionBarButton {}
|
||||
|
||||
let theme = theme::current(cx);
|
||||
let (tooltip_text, action) = tooltip;
|
||||
|
||||
MouseEventHandler::new::<QuickActionBarButton, _>(index, cx, |mouse_state, _| {
|
||||
let style = theme
|
||||
.workspace
|
||||
.toolbar
|
||||
.toggleable_tool
|
||||
.in_state(toggled)
|
||||
.style_for(mouse_state);
|
||||
Svg::new(icon)
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
|
||||
.with_tooltip::<QuickActionBarButton>(index, tooltip_text, action, theme.tooltip.clone(), cx)
|
||||
.into_any_named("quick action bar button")
|
||||
}
|
||||
|
||||
impl ToolbarItemView for QuickActionBar {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
match active_pane_item {
|
||||
Some(active_item) => {
|
||||
self.active_item = Some(active_item.boxed_clone());
|
||||
self._inlay_hints_enabled_subscription.take();
|
||||
|
||||
if let Some(editor) = active_item.downcast::<Editor>() {
|
||||
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
|
||||
self._inlay_hints_enabled_subscription =
|
||||
Some(cx.observe(&editor, move |_, editor, cx| {
|
||||
let new_inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
|
||||
if inlay_hints_enabled != new_inlay_hints_enabled {
|
||||
inlay_hints_enabled = new_inlay_hints_enabled;
|
||||
cx.notify();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
ToolbarItemLocation::PrimaryRight { flex: None }
|
||||
}
|
||||
None => {
|
||||
self.active_item = None;
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -102,17 +102,6 @@ message Envelope {
|
|||
SearchProject search_project = 80;
|
||||
SearchProjectResponse search_project_response = 81;
|
||||
|
||||
GetChannels get_channels = 82;
|
||||
GetChannelsResponse get_channels_response = 83;
|
||||
JoinChannel join_channel = 84;
|
||||
JoinChannelResponse join_channel_response = 85;
|
||||
LeaveChannel leave_channel = 86;
|
||||
SendChannelMessage send_channel_message = 87;
|
||||
SendChannelMessageResponse send_channel_message_response = 88;
|
||||
ChannelMessageSent channel_message_sent = 89;
|
||||
GetChannelMessages get_channel_messages = 90;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 91;
|
||||
|
||||
UpdateContacts update_contacts = 92;
|
||||
UpdateInviteInfo update_invite_info = 93;
|
||||
ShowContacts show_contacts = 94;
|
||||
|
@ -140,6 +129,19 @@ message Envelope {
|
|||
InlayHints inlay_hints = 116;
|
||||
InlayHintsResponse inlay_hints_response = 117;
|
||||
RefreshInlayHints refresh_inlay_hints = 118;
|
||||
|
||||
CreateChannel create_channel = 119;
|
||||
ChannelResponse channel_response = 120;
|
||||
InviteChannelMember invite_channel_member = 121;
|
||||
RemoveChannelMember remove_channel_member = 122;
|
||||
RespondToChannelInvite respond_to_channel_invite = 123;
|
||||
UpdateChannels update_channels = 124;
|
||||
JoinChannel join_channel = 125;
|
||||
RemoveChannel remove_channel = 126;
|
||||
GetChannelMembers get_channel_members = 127;
|
||||
GetChannelMembersResponse get_channel_members_response = 128;
|
||||
SetChannelMemberAdmin set_channel_member_admin = 129;
|
||||
RenameChannel rename_channel = 130;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,7 +176,8 @@ message JoinRoom {
|
|||
|
||||
message JoinRoomResponse {
|
||||
Room room = 1;
|
||||
optional LiveKitConnectionInfo live_kit_connection_info = 2;
|
||||
optional uint64 channel_id = 2;
|
||||
optional LiveKitConnectionInfo live_kit_connection_info = 3;
|
||||
}
|
||||
|
||||
message RejoinRoom {
|
||||
|
@ -867,25 +870,89 @@ message LspDiskBasedDiagnosticsUpdating {}
|
|||
|
||||
message LspDiskBasedDiagnosticsUpdated {}
|
||||
|
||||
message GetChannels {}
|
||||
|
||||
message GetChannelsResponse {
|
||||
message UpdateChannels {
|
||||
repeated Channel channels = 1;
|
||||
repeated uint64 remove_channels = 2;
|
||||
repeated Channel channel_invitations = 3;
|
||||
repeated uint64 remove_channel_invitations = 4;
|
||||
repeated ChannelParticipants channel_participants = 5;
|
||||
repeated ChannelPermission channel_permissions = 6;
|
||||
}
|
||||
|
||||
message ChannelPermission {
|
||||
uint64 channel_id = 1;
|
||||
bool is_admin = 2;
|
||||
}
|
||||
|
||||
message ChannelParticipants {
|
||||
uint64 channel_id = 1;
|
||||
repeated uint64 participant_user_ids = 2;
|
||||
}
|
||||
|
||||
message JoinChannel {
|
||||
uint64 channel_id = 1;
|
||||
}
|
||||
|
||||
message JoinChannelResponse {
|
||||
repeated ChannelMessage messages = 1;
|
||||
bool done = 2;
|
||||
message RemoveChannel {
|
||||
uint64 channel_id = 1;
|
||||
}
|
||||
|
||||
message LeaveChannel {
|
||||
message GetChannelMembers {
|
||||
uint64 channel_id = 1;
|
||||
}
|
||||
|
||||
message GetChannelMembersResponse {
|
||||
repeated ChannelMember members = 1;
|
||||
}
|
||||
|
||||
message ChannelMember {
|
||||
uint64 user_id = 1;
|
||||
bool admin = 2;
|
||||
Kind kind = 3;
|
||||
|
||||
enum Kind {
|
||||
Member = 0;
|
||||
Invitee = 1;
|
||||
AncestorMember = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message CreateChannel {
|
||||
string name = 1;
|
||||
optional uint64 parent_id = 2;
|
||||
}
|
||||
|
||||
message ChannelResponse {
|
||||
Channel channel = 1;
|
||||
}
|
||||
|
||||
message InviteChannelMember {
|
||||
uint64 channel_id = 1;
|
||||
uint64 user_id = 2;
|
||||
bool admin = 3;
|
||||
}
|
||||
|
||||
message RemoveChannelMember {
|
||||
uint64 channel_id = 1;
|
||||
uint64 user_id = 2;
|
||||
}
|
||||
|
||||
message SetChannelMemberAdmin {
|
||||
uint64 channel_id = 1;
|
||||
uint64 user_id = 2;
|
||||
bool admin = 3;
|
||||
}
|
||||
|
||||
message RenameChannel {
|
||||
uint64 channel_id = 1;
|
||||
string name = 2;
|
||||
}
|
||||
|
||||
message RespondToChannelInvite {
|
||||
uint64 channel_id = 1;
|
||||
bool accept = 2;
|
||||
}
|
||||
|
||||
message GetUsers {
|
||||
repeated uint64 user_ids = 1;
|
||||
}
|
||||
|
@ -918,31 +985,6 @@ enum ContactRequestResponse {
|
|||
Dismiss = 3;
|
||||
}
|
||||
|
||||
message SendChannelMessage {
|
||||
uint64 channel_id = 1;
|
||||
string body = 2;
|
||||
Nonce nonce = 3;
|
||||
}
|
||||
|
||||
message SendChannelMessageResponse {
|
||||
ChannelMessage message = 1;
|
||||
}
|
||||
|
||||
message ChannelMessageSent {
|
||||
uint64 channel_id = 1;
|
||||
ChannelMessage message = 2;
|
||||
}
|
||||
|
||||
message GetChannelMessages {
|
||||
uint64 channel_id = 1;
|
||||
uint64 before_message_id = 2;
|
||||
}
|
||||
|
||||
message GetChannelMessagesResponse {
|
||||
repeated ChannelMessage messages = 1;
|
||||
bool done = 2;
|
||||
}
|
||||
|
||||
message UpdateContacts {
|
||||
repeated Contact contacts = 1;
|
||||
repeated uint64 remove_contacts = 2;
|
||||
|
@ -1274,14 +1316,7 @@ message Nonce {
|
|||
message Channel {
|
||||
uint64 id = 1;
|
||||
string name = 2;
|
||||
}
|
||||
|
||||
message ChannelMessage {
|
||||
uint64 id = 1;
|
||||
string body = 2;
|
||||
uint64 timestamp = 3;
|
||||
uint64 sender_id = 4;
|
||||
Nonce nonce = 5;
|
||||
optional uint64 parent_id = 3;
|
||||
}
|
||||
|
||||
message Contact {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(non_snake_case)]
|
||||
|
||||
use super::{entity_messages, messages, request_messages, ConnectionId, TypedEnvelope};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_tungstenite::tungstenite::Message as WebSocketMessage;
|
||||
|
@ -141,9 +143,10 @@ messages!(
|
|||
(Call, Foreground),
|
||||
(CallCanceled, Foreground),
|
||||
(CancelCall, Foreground),
|
||||
(ChannelMessageSent, Foreground),
|
||||
(CopyProjectEntry, Foreground),
|
||||
(CreateBufferForPeer, Foreground),
|
||||
(CreateChannel, Foreground),
|
||||
(ChannelResponse, Foreground),
|
||||
(CreateProjectEntry, Foreground),
|
||||
(CreateRoom, Foreground),
|
||||
(CreateRoomResponse, Foreground),
|
||||
|
@ -156,10 +159,6 @@ messages!(
|
|||
(FormatBuffers, Foreground),
|
||||
(FormatBuffersResponse, Foreground),
|
||||
(FuzzySearchUsers, Foreground),
|
||||
(GetChannelMessages, Foreground),
|
||||
(GetChannelMessagesResponse, Foreground),
|
||||
(GetChannels, Foreground),
|
||||
(GetChannelsResponse, Foreground),
|
||||
(GetCodeActions, Background),
|
||||
(GetCodeActionsResponse, Background),
|
||||
(GetHover, Background),
|
||||
|
@ -179,14 +178,12 @@ messages!(
|
|||
(GetUsers, Foreground),
|
||||
(Hello, Foreground),
|
||||
(IncomingCall, Foreground),
|
||||
(InviteChannelMember, Foreground),
|
||||
(UsersResponse, Foreground),
|
||||
(JoinChannel, Foreground),
|
||||
(JoinChannelResponse, Foreground),
|
||||
(JoinProject, Foreground),
|
||||
(JoinProjectResponse, Foreground),
|
||||
(JoinRoom, Foreground),
|
||||
(JoinRoomResponse, Foreground),
|
||||
(LeaveChannel, Foreground),
|
||||
(LeaveProject, Foreground),
|
||||
(LeaveRoom, Foreground),
|
||||
(OpenBufferById, Background),
|
||||
|
@ -209,18 +206,21 @@ messages!(
|
|||
(RejoinRoom, Foreground),
|
||||
(RejoinRoomResponse, Foreground),
|
||||
(RemoveContact, Foreground),
|
||||
(RemoveChannelMember, Foreground),
|
||||
(ReloadBuffers, Foreground),
|
||||
(ReloadBuffersResponse, Foreground),
|
||||
(RemoveProjectCollaborator, Foreground),
|
||||
(RenameProjectEntry, Foreground),
|
||||
(RequestContact, Foreground),
|
||||
(RespondToContactRequest, Foreground),
|
||||
(RespondToChannelInvite, Foreground),
|
||||
(JoinChannel, Foreground),
|
||||
(RoomUpdated, Foreground),
|
||||
(SaveBuffer, Foreground),
|
||||
(RenameChannel, Foreground),
|
||||
(SetChannelMemberAdmin, Foreground),
|
||||
(SearchProject, Background),
|
||||
(SearchProjectResponse, Background),
|
||||
(SendChannelMessage, Foreground),
|
||||
(SendChannelMessageResponse, Foreground),
|
||||
(ShareProject, Foreground),
|
||||
(ShareProjectResponse, Foreground),
|
||||
(ShowContacts, Foreground),
|
||||
|
@ -233,6 +233,8 @@ messages!(
|
|||
(UpdateBuffer, Foreground),
|
||||
(UpdateBufferFile, Foreground),
|
||||
(UpdateContacts, Foreground),
|
||||
(RemoveChannel, Foreground),
|
||||
(UpdateChannels, Foreground),
|
||||
(UpdateDiagnosticSummary, Foreground),
|
||||
(UpdateFollowers, Foreground),
|
||||
(UpdateInviteInfo, Foreground),
|
||||
|
@ -245,6 +247,8 @@ messages!(
|
|||
(UpdateDiffBase, Foreground),
|
||||
(GetPrivateUserInfo, Foreground),
|
||||
(GetPrivateUserInfoResponse, Foreground),
|
||||
(GetChannelMembers, Foreground),
|
||||
(GetChannelMembersResponse, Foreground)
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
|
@ -258,13 +262,12 @@ request_messages!(
|
|||
(CopyProjectEntry, ProjectEntryResponse),
|
||||
(CreateProjectEntry, ProjectEntryResponse),
|
||||
(CreateRoom, CreateRoomResponse),
|
||||
(CreateChannel, ChannelResponse),
|
||||
(DeclineCall, Ack),
|
||||
(DeleteProjectEntry, ProjectEntryResponse),
|
||||
(ExpandProjectEntry, ExpandProjectEntryResponse),
|
||||
(Follow, FollowResponse),
|
||||
(FormatBuffers, FormatBuffersResponse),
|
||||
(GetChannelMessages, GetChannelMessagesResponse),
|
||||
(GetChannels, GetChannelsResponse),
|
||||
(GetCodeActions, GetCodeActionsResponse),
|
||||
(GetHover, GetHoverResponse),
|
||||
(GetCompletions, GetCompletionsResponse),
|
||||
|
@ -276,7 +279,7 @@ request_messages!(
|
|||
(GetProjectSymbols, GetProjectSymbolsResponse),
|
||||
(FuzzySearchUsers, UsersResponse),
|
||||
(GetUsers, UsersResponse),
|
||||
(JoinChannel, JoinChannelResponse),
|
||||
(InviteChannelMember, Ack),
|
||||
(JoinProject, JoinProjectResponse),
|
||||
(JoinRoom, JoinRoomResponse),
|
||||
(LeaveRoom, Ack),
|
||||
|
@ -293,12 +296,18 @@ request_messages!(
|
|||
(RefreshInlayHints, Ack),
|
||||
(ReloadBuffers, ReloadBuffersResponse),
|
||||
(RequestContact, Ack),
|
||||
(RemoveChannelMember, Ack),
|
||||
(RemoveContact, Ack),
|
||||
(RespondToContactRequest, Ack),
|
||||
(RespondToChannelInvite, Ack),
|
||||
(SetChannelMemberAdmin, Ack),
|
||||
(GetChannelMembers, GetChannelMembersResponse),
|
||||
(JoinChannel, JoinRoomResponse),
|
||||
(RemoveChannel, Ack),
|
||||
(RenameProjectEntry, ProjectEntryResponse),
|
||||
(RenameChannel, ChannelResponse),
|
||||
(SaveBuffer, BufferSaved),
|
||||
(SearchProject, SearchProjectResponse),
|
||||
(SendChannelMessage, SendChannelMessageResponse),
|
||||
(ShareProject, ShareProjectResponse),
|
||||
(SynchronizeBuffers, SynchronizeBuffersResponse),
|
||||
(Test, Test),
|
||||
|
@ -361,8 +370,6 @@ entity_messages!(
|
|||
UpdateDiffBase
|
||||
);
|
||||
|
||||
entity_messages!(channel_id, ChannelMessageSent);
|
||||
|
||||
const KIB: usize = 1024;
|
||||
const MIB: usize = KIB * 1024;
|
||||
const MAX_BUFFER_LEN: usize = MIB;
|
||||
|
|
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
|||
pub use peer::*;
|
||||
mod macros;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 59;
|
||||
pub const PROTOCOL_VERSION: u32 = 60;
|
||||
|
|
|
@ -39,7 +39,7 @@ pub enum Event {
|
|||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(BufferSearchBar::deploy);
|
||||
cx.add_action(BufferSearchBar::deploy_bar);
|
||||
cx.add_action(BufferSearchBar::dismiss);
|
||||
cx.add_action(BufferSearchBar::focus_editor);
|
||||
cx.add_action(BufferSearchBar::select_next_match);
|
||||
|
@ -403,6 +403,19 @@ impl BufferSearchBar {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
|
||||
if self.show(cx) {
|
||||
self.search_suggested(cx);
|
||||
if deploy.focus {
|
||||
self.select_query(cx);
|
||||
cx.focus_self();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if self.active_searchable_item.is_none() {
|
||||
return false;
|
||||
|
@ -532,21 +545,16 @@ impl BufferSearchBar {
|
|||
let _ = self.update_matches(cx);
|
||||
cx.notify();
|
||||
}
|
||||
fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
|
||||
|
||||
fn deploy_bar(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
|
||||
let mut propagate_action = true;
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
search_bar.update(cx, |search_bar, cx| {
|
||||
if search_bar.show(cx) {
|
||||
search_bar.search_suggested(cx);
|
||||
if action.focus {
|
||||
search_bar.select_query(cx);
|
||||
cx.focus_self();
|
||||
}
|
||||
if search_bar.deploy(action, cx) {
|
||||
propagate_action = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if propagate_action {
|
||||
cx.propagate_action();
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ db = { path = "../db" }
|
|||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
|
||||
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "7b9f32300ee0a249c0872302c97635b460e45ba5" }
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "f6d001ba8080ebfab6822106a436c64b677a44d5" }
|
||||
procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
|
|
|
@ -400,7 +400,8 @@ impl TerminalElement {
|
|||
region = region
|
||||
// Start selections
|
||||
.on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| {
|
||||
cx.focus_parent();
|
||||
let terminal_view = cx.handle();
|
||||
cx.focus(&terminal_view);
|
||||
v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
|
||||
if let Some(conn_handle) = connection.upgrade(cx) {
|
||||
conn_handle.update(cx, |terminal, cx| {
|
||||
|
|
|
@ -362,10 +362,10 @@ impl Panel for TerminalPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
|
||||
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
|
||||
match self.position(cx) {
|
||||
DockPosition::Left | DockPosition::Right => self.width = Some(size),
|
||||
DockPosition::Bottom => self.height = Some(size),
|
||||
DockPosition::Left | DockPosition::Right => self.width = size,
|
||||
DockPosition::Bottom => self.height = size,
|
||||
}
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
|
@ -393,8 +393,8 @@ impl Panel for TerminalPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn icon_path(&self) -> &'static str {
|
||||
"icons/terminal_12.svg"
|
||||
fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
|
||||
Some("icons/terminal.svg")
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
|
||||
|
|
|
@ -671,7 +671,7 @@ impl Item for TerminalView {
|
|||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
gpui::elements::Svg::new("icons/terminal_12.svg")
|
||||
gpui::elements::Svg::new("icons/terminal.svg")
|
||||
.with_color(tab_theme.label.text.color)
|
||||
.constrained()
|
||||
.with_width(tab_theme.type_icon_width)
|
||||
|
|
|
@ -43,11 +43,9 @@ pub struct Theme {
|
|||
pub meta: ThemeMeta,
|
||||
pub workspace: Workspace,
|
||||
pub context_menu: ContextMenu,
|
||||
pub contacts_popover: ContactsPopover,
|
||||
pub contact_list: ContactList,
|
||||
pub toolbar_dropdown_menu: DropdownMenu,
|
||||
pub copilot: Copilot,
|
||||
pub contact_finder: ContactFinder,
|
||||
pub collab_panel: CollabPanel,
|
||||
pub project_panel: ProjectPanel,
|
||||
pub command_palette: CommandPalette,
|
||||
pub picker: Picker,
|
||||
|
@ -117,6 +115,7 @@ pub struct Titlebar {
|
|||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub height: f32,
|
||||
pub menu: TitlebarMenu,
|
||||
pub project_menu_button: Toggleable<Interactive<ContainedText>>,
|
||||
pub project_name_divider: ContainedText,
|
||||
pub git_menu_button: Toggleable<Interactive<ContainedText>>,
|
||||
|
@ -143,6 +142,12 @@ pub struct Titlebar {
|
|||
pub user_menu: UserMenu,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct TitlebarMenu {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct UserMenu {
|
||||
pub user_menu_button_online: UserMenuButton,
|
||||
|
@ -211,33 +216,69 @@ pub struct CopilotAuthAuthorized {
|
|||
}
|
||||
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ContactsPopover {
|
||||
pub struct CollabPanel {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub height: f32,
|
||||
pub width: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ContactList {
|
||||
pub list_empty_state: Toggleable<Interactive<ContainedText>>,
|
||||
pub list_empty_icon: Icon,
|
||||
pub list_empty_label_container: ContainerStyle,
|
||||
pub log_in_button: Interactive<ContainedText>,
|
||||
pub channel_editor: ContainerStyle,
|
||||
pub channel_hash: Icon,
|
||||
pub tabbed_modal: TabbedModal,
|
||||
pub contact_finder: ContactFinder,
|
||||
pub channel_modal: ChannelModal,
|
||||
pub user_query_editor: FieldEditor,
|
||||
pub user_query_editor_height: f32,
|
||||
pub add_contact_button: IconButton,
|
||||
pub header_row: Toggleable<Interactive<ContainedText>>,
|
||||
pub leave_call_button: Toggleable<Interactive<IconButton>>,
|
||||
pub add_contact_button: Toggleable<Interactive<IconButton>>,
|
||||
pub add_channel_button: Toggleable<Interactive<IconButton>>,
|
||||
pub header_row: ContainedText,
|
||||
pub subheader_row: Toggleable<Interactive<ContainedText>>,
|
||||
pub leave_call: Interactive<ContainedText>,
|
||||
pub contact_row: Toggleable<Interactive<ContainerStyle>>,
|
||||
pub channel_row: Toggleable<Interactive<ContainerStyle>>,
|
||||
pub channel_name: ContainedText,
|
||||
pub row_height: f32,
|
||||
pub project_row: Toggleable<Interactive<ProjectRow>>,
|
||||
pub tree_branch: Toggleable<Interactive<TreeBranch>>,
|
||||
pub contact_avatar: ImageStyle,
|
||||
pub channel_avatar: ImageStyle,
|
||||
pub extra_participant_label: ContainedText,
|
||||
pub contact_status_free: ContainerStyle,
|
||||
pub contact_status_busy: ContainerStyle,
|
||||
pub contact_username: ContainedText,
|
||||
pub contact_button: Interactive<IconButton>,
|
||||
pub contact_button_spacing: f32,
|
||||
pub channel_indent: f32,
|
||||
pub disabled_button: IconButton,
|
||||
pub section_icon_size: f32,
|
||||
pub calling_indicator: ContainedText,
|
||||
pub face_overlap: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct TabbedModal {
|
||||
pub tab_button: Toggleable<Interactive<ContainedText>>,
|
||||
pub modal: ContainerStyle,
|
||||
pub header: ContainerStyle,
|
||||
pub body: ContainerStyle,
|
||||
pub title: ContainedText,
|
||||
pub picker: Picker,
|
||||
pub max_height: f32,
|
||||
pub max_width: f32,
|
||||
pub row_height: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ChannelModal {
|
||||
pub contact_avatar: ImageStyle,
|
||||
pub contact_username: ContainerStyle,
|
||||
pub remove_member_button: ContainedText,
|
||||
pub cancel_invite_button: ContainedText,
|
||||
pub member_icon: IconButton,
|
||||
pub invitee_icon: IconButton,
|
||||
pub member_tag: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
|
@ -256,8 +297,6 @@ pub struct TreeBranch {
|
|||
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ContactFinder {
|
||||
pub picker: Picker,
|
||||
pub row_height: f32,
|
||||
pub contact_avatar: ImageStyle,
|
||||
pub contact_username: ContainerStyle,
|
||||
pub contact_button: IconButton,
|
||||
|
@ -360,6 +399,7 @@ pub struct Toolbar {
|
|||
pub container: ContainerStyle,
|
||||
pub height: f32,
|
||||
pub item_spacing: f32,
|
||||
pub toggleable_tool: Toggleable<Interactive<IconButton>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
|
@ -867,6 +907,7 @@ impl<T> Toggleable<T> {
|
|||
pub fn active_state(&self) -> &T {
|
||||
self.in_state(true)
|
||||
}
|
||||
|
||||
pub fn inactive_state(&self) -> &T {
|
||||
self.in_state(false)
|
||||
}
|
||||
|
@ -887,6 +928,16 @@ impl<T> Interactive<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> Toggleable<Interactive<T>> {
|
||||
pub fn style_for(&self, active: bool, state: &mut MouseState) -> &T {
|
||||
self.in_state(active).style_for(state)
|
||||
}
|
||||
|
||||
pub fn default_style(&self) -> &T {
|
||||
&self.inactive.default
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
|
@ -1052,6 +1103,12 @@ pub struct AssistantStyle {
|
|||
pub saved_conversation: SavedConversation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Contained<T> {
|
||||
container: ContainerStyle,
|
||||
contained: T,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct SavedConversation {
|
||||
pub container: Interactive<ContainerStyle>,
|
||||
|
|
|
@ -107,6 +107,16 @@ pub struct IconStyle {
|
|||
pub container: ContainerStyle,
|
||||
}
|
||||
|
||||
impl IconStyle {
|
||||
pub fn width(&self) -> f32 {
|
||||
self.icon.dimensions.width
|
||||
+ self.container.padding.left
|
||||
+ self.container.padding.right
|
||||
+ self.container.margin.left
|
||||
+ self.container.margin.right
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon<V: View>(style: &IconStyle) -> Container<V> {
|
||||
svg(&style.icon).contained().with_style(style.container)
|
||||
}
|
||||
|
|
|
@ -256,7 +256,7 @@ impl PickerDelegate for BranchListDelegate {
|
|||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(theme.contact_finder.row_height)
|
||||
.with_height(theme.collab_panel.tabbed_modal.row_height)
|
||||
.into_any()
|
||||
}
|
||||
fn render_header(
|
||||
|
|
|
@ -92,6 +92,7 @@ impl<'a> VimTestContext<'a> {
|
|||
vim.switch_mode(mode, true, cx);
|
||||
})
|
||||
});
|
||||
self.cx.foreground().run_until_parked();
|
||||
context_handle
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{StatusItemView, Workspace};
|
||||
use crate::{StatusItemView, Workspace, WorkspaceBounds};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use gpui::{
|
||||
elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyViewHandle, AppContext,
|
||||
|
@ -13,20 +13,30 @@ pub trait Panel: View {
|
|||
fn position_is_valid(&self, position: DockPosition) -> bool;
|
||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
|
||||
fn size(&self, cx: &WindowContext) -> f32;
|
||||
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>);
|
||||
fn icon_path(&self) -> &'static str;
|
||||
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
|
||||
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
|
||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>);
|
||||
fn icon_label(&self, _: &WindowContext) -> Option<String> {
|
||||
None
|
||||
}
|
||||
fn should_change_position_on_event(_: &Self::Event) -> bool;
|
||||
fn should_zoom_in_on_event(_: &Self::Event) -> bool;
|
||||
fn should_zoom_out_on_event(_: &Self::Event) -> bool;
|
||||
fn is_zoomed(&self, cx: &WindowContext) -> bool;
|
||||
fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>);
|
||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>);
|
||||
fn should_activate_on_event(_: &Self::Event) -> bool;
|
||||
fn should_close_on_event(_: &Self::Event) -> bool;
|
||||
fn should_zoom_in_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
fn should_zoom_out_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
fn is_zoomed(&self, _cx: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
|
||||
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
|
||||
fn should_activate_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
fn should_close_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
fn has_focus(&self, cx: &WindowContext) -> bool;
|
||||
fn is_focus_event(_: &Self::Event) -> bool;
|
||||
}
|
||||
|
@ -40,8 +50,8 @@ pub trait PanelHandle {
|
|||
fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
|
||||
fn set_active(&self, active: bool, cx: &mut WindowContext);
|
||||
fn size(&self, cx: &WindowContext) -> f32;
|
||||
fn set_size(&self, size: f32, cx: &mut WindowContext);
|
||||
fn icon_path(&self, cx: &WindowContext) -> &'static str;
|
||||
fn set_size(&self, size: Option<f32>, cx: &mut WindowContext);
|
||||
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
|
||||
fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
|
||||
fn icon_label(&self, cx: &WindowContext) -> Option<String>;
|
||||
fn has_focus(&self, cx: &WindowContext) -> bool;
|
||||
|
@ -72,7 +82,7 @@ where
|
|||
self.read(cx).size(cx)
|
||||
}
|
||||
|
||||
fn set_size(&self, size: f32, cx: &mut WindowContext) {
|
||||
fn set_size(&self, size: Option<f32>, cx: &mut WindowContext) {
|
||||
self.update(cx, |this, cx| this.set_size(size, cx))
|
||||
}
|
||||
|
||||
|
@ -88,8 +98,8 @@ where
|
|||
self.update(cx, |this, cx| this.set_active(active, cx))
|
||||
}
|
||||
|
||||
fn icon_path(&self, cx: &WindowContext) -> &'static str {
|
||||
self.read(cx).icon_path()
|
||||
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
|
||||
self.read(cx).icon_path(cx)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>) {
|
||||
|
@ -363,7 +373,7 @@ impl Dock {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn resize_active_panel(&mut self, size: f32, cx: &mut ViewContext<Self>) {
|
||||
pub fn resize_active_panel(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
|
||||
if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
|
||||
entry.panel.set_size(size, cx);
|
||||
cx.notify();
|
||||
|
@ -376,7 +386,7 @@ impl Dock {
|
|||
.into_any()
|
||||
.contained()
|
||||
.with_style(self.style(cx))
|
||||
.resizable(
|
||||
.resizable::<WorkspaceBounds>(
|
||||
self.position.to_resize_handle_side(),
|
||||
active_entry.panel.size(cx),
|
||||
|_, _, _| {},
|
||||
|
@ -413,7 +423,7 @@ impl View for Dock {
|
|||
ChildView::new(active_entry.panel.as_any(), cx)
|
||||
.contained()
|
||||
.with_style(style)
|
||||
.resizable(
|
||||
.resizable::<WorkspaceBounds>(
|
||||
self.position.to_resize_handle_side(),
|
||||
active_entry.panel.size(cx),
|
||||
|dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
|
||||
|
@ -480,8 +490,9 @@ impl View for PanelButtons {
|
|||
.map(|item| (item.panel.clone(), item.context_menu.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
Flex::row()
|
||||
.with_children(panels.into_iter().enumerate().map(
|
||||
.with_children(panels.into_iter().enumerate().filter_map(
|
||||
|(panel_ix, (view, context_menu))| {
|
||||
let icon_path = view.icon_path(cx)?;
|
||||
let is_active = is_open && panel_ix == active_ix;
|
||||
let (tooltip, tooltip_action) = if is_active {
|
||||
(
|
||||
|
@ -495,92 +506,95 @@ impl View for PanelButtons {
|
|||
} else {
|
||||
view.icon_tooltip(cx)
|
||||
};
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Self, _>(panel_ix, cx, |state, cx| {
|
||||
let style = button_style.in_state(is_active);
|
||||
let style = style.style_for(state);
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new(view.icon_path(cx))
|
||||
.with_color(style.icon_color)
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.aligned(),
|
||||
)
|
||||
.with_children(if let Some(label) = view.icon_label(cx) {
|
||||
Some(
|
||||
Label::new(label, style.label.text.clone())
|
||||
.contained()
|
||||
.with_style(style.label.container)
|
||||
Some(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Self, _>(panel_ix, cx, |state, cx| {
|
||||
let style = button_style.in_state(is_active);
|
||||
|
||||
let style = style.style_for(state);
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new(icon_path)
|
||||
.with_color(style.icon_color)
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.aligned(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.constrained()
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, {
|
||||
let tooltip_action =
|
||||
tooltip_action.as_ref().map(|action| action.boxed_clone());
|
||||
move |_, this, cx| {
|
||||
if let Some(tooltip_action) = &tooltip_action {
|
||||
let window = cx.window();
|
||||
let view_id = this.workspace.id();
|
||||
let tooltip_action = tooltip_action.boxed_clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
window.dispatch_action(
|
||||
view_id,
|
||||
&*tooltip_action,
|
||||
&mut cx,
|
||||
);
|
||||
.with_children(if let Some(label) = view.icon_label(cx) {
|
||||
Some(
|
||||
Label::new(label, style.label.text.clone())
|
||||
.contained()
|
||||
.with_style(style.label.container)
|
||||
.aligned(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.detach();
|
||||
.constrained()
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, {
|
||||
let tooltip_action =
|
||||
tooltip_action.as_ref().map(|action| action.boxed_clone());
|
||||
move |_, this, cx| {
|
||||
if let Some(tooltip_action) = &tooltip_action {
|
||||
let window = cx.window();
|
||||
let view_id = this.workspace.id();
|
||||
let tooltip_action = tooltip_action.boxed_clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
window.dispatch_action(
|
||||
view_id,
|
||||
&*tooltip_action,
|
||||
&mut cx,
|
||||
);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_click(MouseButton::Right, {
|
||||
let view = view.clone();
|
||||
let menu = context_menu.clone();
|
||||
move |_, _, cx| {
|
||||
const POSITIONS: [DockPosition; 3] = [
|
||||
DockPosition::Left,
|
||||
DockPosition::Right,
|
||||
DockPosition::Bottom,
|
||||
];
|
||||
})
|
||||
.on_click(MouseButton::Right, {
|
||||
let view = view.clone();
|
||||
let menu = context_menu.clone();
|
||||
move |_, _, cx| {
|
||||
const POSITIONS: [DockPosition; 3] = [
|
||||
DockPosition::Left,
|
||||
DockPosition::Right,
|
||||
DockPosition::Bottom,
|
||||
];
|
||||
|
||||
menu.update(cx, |menu, cx| {
|
||||
let items = POSITIONS
|
||||
.into_iter()
|
||||
.filter(|position| {
|
||||
*position != dock_position
|
||||
&& view.position_is_valid(*position, cx)
|
||||
})
|
||||
.map(|position| {
|
||||
let view = view.clone();
|
||||
ContextMenuItem::handler(
|
||||
format!("Dock {}", position.to_label()),
|
||||
move |cx| view.set_position(position, cx),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
menu.show(Default::default(), menu_corner, items, cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self>(
|
||||
panel_ix,
|
||||
tooltip,
|
||||
tooltip_action,
|
||||
tooltip_style.clone(),
|
||||
cx,
|
||||
),
|
||||
)
|
||||
.with_child(ChildView::new(&context_menu, cx))
|
||||
menu.update(cx, |menu, cx| {
|
||||
let items = POSITIONS
|
||||
.into_iter()
|
||||
.filter(|position| {
|
||||
*position != dock_position
|
||||
&& view.position_is_valid(*position, cx)
|
||||
})
|
||||
.map(|position| {
|
||||
let view = view.clone();
|
||||
ContextMenuItem::handler(
|
||||
format!("Dock {}", position.to_label()),
|
||||
move |cx| view.set_position(position, cx),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
menu.show(Default::default(), menu_corner, items, cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self>(
|
||||
panel_ix,
|
||||
tooltip,
|
||||
tooltip_action,
|
||||
tooltip_style.clone(),
|
||||
cx,
|
||||
),
|
||||
)
|
||||
.with_child(ChildView::new(&context_menu, cx)),
|
||||
)
|
||||
},
|
||||
))
|
||||
.contained()
|
||||
|
@ -686,12 +700,12 @@ pub mod test {
|
|||
self.size
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: f32, _: &mut ViewContext<Self>) {
|
||||
self.size = size;
|
||||
fn set_size(&mut self, size: Option<f32>, _: &mut ViewContext<Self>) {
|
||||
self.size = size.unwrap_or(300.);
|
||||
}
|
||||
|
||||
fn icon_path(&self) -> &'static str {
|
||||
"icons/test_panel.svg"
|
||||
fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
|
||||
Some("icons/test_panel.svg")
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
|
||||
|
|
|
@ -14,7 +14,7 @@ use anyhow::{anyhow, Context, Result};
|
|||
use call::ActiveCall;
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Client, TypedEnvelope, UserStore,
|
||||
ChannelStore, Client, TypedEnvelope, UserStore,
|
||||
};
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use drag_and_drop::DragAndDrop;
|
||||
|
@ -400,8 +400,9 @@ pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
|
|||
|
||||
pub struct AppState {
|
||||
pub languages: Arc<LanguageRegistry>,
|
||||
pub client: Arc<client::Client>,
|
||||
pub user_store: ModelHandle<client::UserStore>,
|
||||
pub client: Arc<Client>,
|
||||
pub user_store: ModelHandle<UserStore>,
|
||||
pub channel_store: ModelHandle<ChannelStore>,
|
||||
pub fs: Arc<dyn fs::Fs>,
|
||||
pub build_window_options:
|
||||
fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
|
||||
|
@ -424,6 +425,8 @@ impl AppState {
|
|||
let http_client = util::http::FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http_client.clone(), cx);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||
let channel_store =
|
||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||
|
||||
theme::init((), cx);
|
||||
client::init(&client, cx);
|
||||
|
@ -434,6 +437,7 @@ impl AppState {
|
|||
fs,
|
||||
languages,
|
||||
user_store,
|
||||
channel_store,
|
||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
background_actions: || &[],
|
||||
|
@ -549,6 +553,8 @@ struct FollowerState {
|
|||
items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
|
||||
}
|
||||
|
||||
enum WorkspaceBounds {}
|
||||
|
||||
impl Workspace {
|
||||
pub fn new(
|
||||
workspace_id: WorkspaceId,
|
||||
|
@ -3403,10 +3409,16 @@ impl Workspace {
|
|||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let client = project.read(cx).client();
|
||||
let user_store = project.read(cx).user_store();
|
||||
|
||||
let channel_store =
|
||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||
let app_state = Arc::new(AppState {
|
||||
languages: project.read(cx).languages().clone(),
|
||||
client: project.read(cx).client(),
|
||||
user_store: project.read(cx).user_store(),
|
||||
client,
|
||||
user_store,
|
||||
channel_store,
|
||||
fs: project.read(cx).fs().clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||
|
@ -3750,14 +3762,23 @@ impl View for Workspace {
|
|||
)
|
||||
}))
|
||||
.with_children(self.modal.as_ref().map(|modal| {
|
||||
ChildView::new(modal.view.as_any(), cx)
|
||||
.contained()
|
||||
.with_style(theme.workspace.modal)
|
||||
.aligned()
|
||||
.top()
|
||||
// Prevent clicks within the modal from falling
|
||||
// through to the rest of the workspace.
|
||||
enum ModalBackground {}
|
||||
MouseEventHandler::new::<ModalBackground, _>(
|
||||
0,
|
||||
cx,
|
||||
|_, cx| ChildView::new(modal.view.as_any(), cx),
|
||||
)
|
||||
.on_click(MouseButton::Left, |_, _, _| {})
|
||||
.contained()
|
||||
.with_style(theme.workspace.modal)
|
||||
.aligned()
|
||||
.top()
|
||||
}))
|
||||
.with_children(self.render_notifications(&theme.workspace, cx)),
|
||||
))
|
||||
.provide_resize_bounds::<WorkspaceBounds>()
|
||||
.flex(1.0, true),
|
||||
)
|
||||
.with_child(ChildView::new(&self.status_bar, cx))
|
||||
|
@ -4841,7 +4862,9 @@ mod tests {
|
|||
panel_1.size(cx)
|
||||
);
|
||||
|
||||
left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
|
||||
left_dock.update(cx, |left_dock, cx| {
|
||||
left_dock.resize_active_panel(Some(1337.), cx)
|
||||
});
|
||||
assert_eq!(
|
||||
workspace
|
||||
.right_dock()
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.100.0"
|
||||
version = "0.101.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
|
@ -54,6 +54,7 @@ plugin_runtime = { path = "../plugin_runtime",optional = true }
|
|||
project = { path = "../project" }
|
||||
project_panel = { path = "../project_panel" }
|
||||
project_symbols = { path = "../project_symbols" }
|
||||
quick_action_bar = { path = "../quick_action_bar" }
|
||||
recent_projects = { path = "../recent_projects" }
|
||||
rpc = { path = "../rpc" }
|
||||
settings = { path = "../settings" }
|
||||
|
|
|
@ -7,7 +7,9 @@ use cli::{
|
|||
ipc::{self, IpcSender},
|
||||
CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
|
||||
};
|
||||
use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
|
||||
use client::{
|
||||
self, ChannelStore, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
|
||||
};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
||||
use futures::{
|
||||
|
@ -140,6 +142,8 @@ fn main() {
|
|||
|
||||
languages::init(languages.clone(), node_runtime.clone());
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
|
||||
let channel_store =
|
||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||
|
||||
cx.set_global(client.clone());
|
||||
|
||||
|
@ -181,6 +185,7 @@ fn main() {
|
|||
languages,
|
||||
client: client.clone(),
|
||||
user_store,
|
||||
channel_store,
|
||||
fs,
|
||||
build_window_options,
|
||||
initialize_workspace,
|
||||
|
|
|
@ -10,7 +10,7 @@ use anyhow::Context;
|
|||
use assets::Assets;
|
||||
use breadcrumbs::Breadcrumbs;
|
||||
pub use client;
|
||||
use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
|
||||
use collab_ui::CollabTitlebarItem; // TODO: Add back toggle collab ui shortcut
|
||||
use collections::VecDeque;
|
||||
pub use editor;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
|
@ -30,6 +30,7 @@ use gpui::{
|
|||
pub use lsp;
|
||||
pub use project;
|
||||
use project_panel::ProjectPanel;
|
||||
use quick_action_bar::QuickActionBar;
|
||||
use search::{BufferSearchBar, ProjectSearchBar};
|
||||
use serde::Deserialize;
|
||||
use serde_json::to_string_pretty;
|
||||
|
@ -85,20 +86,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|
|||
cx.toggle_full_screen();
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace, _: &ToggleContactsMenu, cx: &mut ViewContext<Workspace>| {
|
||||
if let Some(item) = workspace
|
||||
.titlebar_item()
|
||||
.and_then(|item| item.downcast::<CollabTitlebarItem>())
|
||||
{
|
||||
cx.defer(move |_, cx| {
|
||||
item.update(cx, |item, cx| {
|
||||
item.toggle_contacts_popover(&Default::default(), cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
cx.add_global_action(quit);
|
||||
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
|
||||
cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
|
||||
|
@ -220,6 +207,13 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|
|||
workspace.toggle_panel_focus::<ProjectPanel>(cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace,
|
||||
_: &collab_ui::collab_panel::ToggleFocus,
|
||||
cx: &mut ViewContext<Workspace>| {
|
||||
workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace,
|
||||
_: &terminal_panel::ToggleFocus,
|
||||
|
@ -269,7 +263,10 @@ pub fn initialize_workspace(
|
|||
let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
|
||||
toolbar.add_item(breadcrumbs, cx);
|
||||
let buffer_search_bar = cx.add_view(BufferSearchBar::new);
|
||||
toolbar.add_item(buffer_search_bar, cx);
|
||||
toolbar.add_item(buffer_search_bar.clone(), cx);
|
||||
let quick_action_bar =
|
||||
cx.add_view(|_| QuickActionBar::new(buffer_search_bar));
|
||||
toolbar.add_item(quick_action_bar, cx);
|
||||
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||
toolbar.add_item(project_search_bar, cx);
|
||||
let submit_feedback_button =
|
||||
|
@ -338,9 +335,14 @@ pub fn initialize_workspace(
|
|||
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let (project_panel, terminal_panel, assistant_panel) =
|
||||
futures::try_join!(project_panel, terminal_panel, assistant_panel)?;
|
||||
|
||||
let channels_panel =
|
||||
collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let (project_panel, terminal_panel, assistant_panel, channels_panel) = futures::try_join!(
|
||||
project_panel,
|
||||
terminal_panel,
|
||||
assistant_panel,
|
||||
channels_panel
|
||||
)?;
|
||||
workspace_handle.update(&mut cx, |workspace, cx| {
|
||||
let project_panel_position = project_panel.position(cx);
|
||||
workspace.add_panel_with_extra_event_handler(
|
||||
|
@ -358,6 +360,7 @@ pub fn initialize_workspace(
|
|||
);
|
||||
workspace.add_panel(terminal_panel, cx);
|
||||
workspace.add_panel(assistant_panel, cx);
|
||||
workspace.add_panel(channels_panel, cx);
|
||||
|
||||
if !was_deserialized
|
||||
&& workspace
|
||||
|
@ -2382,6 +2385,7 @@ mod tests {
|
|||
language::init(cx);
|
||||
editor::init(cx);
|
||||
project_panel::init_settings(cx);
|
||||
collab_ui::init(&app_state, cx);
|
||||
pane::init(cx);
|
||||
project_panel::init((), cx);
|
||||
terminal_view::init(cx);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue