mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-27 12:54:42 +00:00
Allow adding write access to guests
This commit is contained in:
parent
ca0c06b577
commit
844d161c40
16 changed files with 349 additions and 145 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1473,6 +1473,7 @@ dependencies = [
|
||||||
"editor",
|
"editor",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"envy",
|
"envy",
|
||||||
|
"file_finder",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"git",
|
"git",
|
||||||
|
@ -1486,6 +1487,7 @@ dependencies = [
|
||||||
"live_kit_server",
|
"live_kit_server",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
|
"menu",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
"node_runtime",
|
"node_runtime",
|
||||||
"notifications",
|
"notifications",
|
||||||
|
|
|
@ -620,6 +620,27 @@ impl Room {
|
||||||
self.local_participant.role == proto::ChannelRole::Admin
|
self.local_participant.role == proto::ChannelRole::Admin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_participant_role(
|
||||||
|
&mut self,
|
||||||
|
user_id: u64,
|
||||||
|
role: proto::ChannelRole,
|
||||||
|
cx: &ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let client = self.client.clone();
|
||||||
|
let room_id = self.id;
|
||||||
|
let role = role.into();
|
||||||
|
cx.spawn(|_, _| async move {
|
||||||
|
client
|
||||||
|
.request(proto::SetRoomParticipantRole {
|
||||||
|
room_id,
|
||||||
|
user_id,
|
||||||
|
role,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn pending_participants(&self) -> &[Arc<User>] {
|
pub fn pending_participants(&self) -> &[Arc<User>] {
|
||||||
&self.pending_participants
|
&self.pending_participants
|
||||||
}
|
}
|
||||||
|
@ -731,7 +752,7 @@ impl Room {
|
||||||
|
|
||||||
this.joined_projects.retain(|project| {
|
this.joined_projects.retain(|project| {
|
||||||
if let Some(project) = project.upgrade() {
|
if let Some(project) = project.upgrade() {
|
||||||
project.update(cx, |project, _| project.set_role(role));
|
project.update(cx, |project, cx| project.set_role(role, cx));
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
|
|
@ -74,6 +74,8 @@ live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
node_runtime = { path = "../node_runtime" }
|
node_runtime = { path = "../node_runtime" }
|
||||||
notifications = { path = "../notifications", features = ["test-support"] }
|
notifications = { path = "../notifications", features = ["test-support"] }
|
||||||
|
file_finder = { path = "../file_finder"}
|
||||||
|
menu = { path = "../menu"}
|
||||||
|
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
rpc = { path = "../rpc", features = ["test-support"] }
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
|
|
|
@ -1004,6 +1004,46 @@ impl Database {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_room_participant_role(
|
||||||
|
&self,
|
||||||
|
admin_id: UserId,
|
||||||
|
room_id: RoomId,
|
||||||
|
user_id: UserId,
|
||||||
|
role: ChannelRole,
|
||||||
|
) -> Result<RoomGuard<proto::Room>> {
|
||||||
|
self.room_transaction(room_id, |tx| async move {
|
||||||
|
room_participant::Entity::find()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(room_participant::Column::RoomId.eq(room_id))
|
||||||
|
.add(room_participant::Column::UserId.eq(admin_id))
|
||||||
|
.add(room_participant::Column::Role.eq(ChannelRole::Admin)),
|
||||||
|
)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("only admins can set participant role"))?;
|
||||||
|
|
||||||
|
let result = room_participant::Entity::update_many()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(room_participant::Column::RoomId.eq(room_id))
|
||||||
|
.add(room_participant::Column::UserId.eq(user_id)),
|
||||||
|
)
|
||||||
|
.set(room_participant::ActiveModel {
|
||||||
|
role: ActiveValue::set(Some(ChannelRole::from(role))),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result.rows_affected != 1 {
|
||||||
|
Err(anyhow!("could not update room participant role"))?;
|
||||||
|
}
|
||||||
|
Ok(self.get_room(room_id, &tx).await?)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> {
|
pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
self.room_connection_lost(connection, &*tx).await?;
|
self.room_connection_lost(connection, &*tx).await?;
|
||||||
|
|
|
@ -202,6 +202,7 @@ impl Server {
|
||||||
.add_request_handler(join_room)
|
.add_request_handler(join_room)
|
||||||
.add_request_handler(rejoin_room)
|
.add_request_handler(rejoin_room)
|
||||||
.add_request_handler(leave_room)
|
.add_request_handler(leave_room)
|
||||||
|
.add_request_handler(set_room_participant_role)
|
||||||
.add_request_handler(call)
|
.add_request_handler(call)
|
||||||
.add_request_handler(cancel_call)
|
.add_request_handler(cancel_call)
|
||||||
.add_message_handler(decline_call)
|
.add_message_handler(decline_call)
|
||||||
|
@ -1258,6 +1259,27 @@ async fn leave_room(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn set_room_participant_role(
|
||||||
|
request: proto::SetRoomParticipantRole,
|
||||||
|
response: Response<proto::SetRoomParticipantRole>,
|
||||||
|
session: Session,
|
||||||
|
) -> Result<()> {
|
||||||
|
let room = session
|
||||||
|
.db()
|
||||||
|
.await
|
||||||
|
.set_room_participant_role(
|
||||||
|
session.user_id,
|
||||||
|
RoomId::from_proto(request.room_id),
|
||||||
|
UserId::from_proto(request.user_id),
|
||||||
|
ChannelRole::from(request.role()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
room_updated(&room, &session.peer);
|
||||||
|
response.send(proto::Ack {})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn call(
|
async fn call(
|
||||||
request: proto::Call,
|
request: proto::Call,
|
||||||
response: Response<proto::Call>,
|
response: Response<proto::Call>,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::tests::TestServer;
|
use crate::tests::TestServer;
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
|
use editor::Editor;
|
||||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||||
use rpc::proto;
|
use rpc::proto;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
@ -13,37 +14,18 @@ async fn test_channel_guests(
|
||||||
let mut server = TestServer::start(executor.clone()).await;
|
let mut server = TestServer::start(executor.clone()).await;
|
||||||
let client_a = server.create_client(cx_a, "user_a").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_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
|
||||||
let channel_id = server
|
|
||||||
.make_channel("the-channel", None, (&client_a, cx_a), &mut [])
|
|
||||||
.await;
|
|
||||||
|
|
||||||
client_a
|
|
||||||
.channel_store()
|
|
||||||
.update(cx_a, |channel_store, cx| {
|
|
||||||
channel_store.set_channel_visibility(channel_id, proto::ChannelVisibility::Public, cx)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
client_a
|
|
||||||
.fs()
|
|
||||||
.insert_tree(
|
|
||||||
"/a",
|
|
||||||
serde_json::json!({
|
|
||||||
"a.txt": "a-contents",
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let active_call_a = cx_a.read(ActiveCall::global);
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
|
||||||
|
let channel_id = server
|
||||||
|
.make_public_channel("the-channel", &client_a, cx_a)
|
||||||
|
.await;
|
||||||
|
|
||||||
// Client A shares a project in the channel
|
// Client A shares a project in the channel
|
||||||
|
let project_a = client_a.build_test_project(cx_a).await;
|
||||||
active_call_a
|
active_call_a
|
||||||
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
|
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
|
||||||
let project_id = active_call_a
|
let project_id = active_call_a
|
||||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
.await
|
.await
|
||||||
|
@ -57,33 +39,16 @@ async fn test_channel_guests(
|
||||||
|
|
||||||
// b should be following a in the shared project.
|
// b should be following a in the shared project.
|
||||||
// B is a guest,
|
// B is a guest,
|
||||||
cx_a.executor().run_until_parked();
|
executor.run_until_parked();
|
||||||
|
|
||||||
// todo!() the test window does not call activation handlers
|
|
||||||
// correctly yet, so this API does not work.
|
|
||||||
// let project_b = active_call_b.read_with(cx_b, |call, _| {
|
|
||||||
// call.location()
|
|
||||||
// .unwrap()
|
|
||||||
// .upgrade()
|
|
||||||
// .expect("should not be weak")
|
|
||||||
// });
|
|
||||||
|
|
||||||
let window_b = cx_b.update(|cx| cx.active_window().unwrap());
|
|
||||||
let cx_b = &mut VisualTestContext::from_window(window_b, cx_b);
|
|
||||||
|
|
||||||
let workspace_b = window_b
|
|
||||||
.downcast::<Workspace>()
|
|
||||||
.unwrap()
|
|
||||||
.root_view(cx_b)
|
|
||||||
.unwrap();
|
|
||||||
let project_b = workspace_b.update(cx_b, |workspace, _| workspace.project().clone());
|
|
||||||
|
|
||||||
|
let active_call_b = cx_b.read(ActiveCall::global);
|
||||||
|
let project_b =
|
||||||
|
active_call_b.read_with(cx_b, |call, _| call.location().unwrap().upgrade().unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
project_b.read_with(cx_b, |project, _| project.remote_id()),
|
project_b.read_with(cx_b, |project, _| project.remote_id()),
|
||||||
Some(project_id),
|
Some(project_id),
|
||||||
);
|
);
|
||||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||||
|
|
||||||
assert!(project_b
|
assert!(project_b
|
||||||
.update(cx_b, |project, cx| {
|
.update(cx_b, |project, cx| {
|
||||||
let worktree_id = project.worktrees().next().unwrap().read(cx).id();
|
let worktree_id = project.worktrees().next().unwrap().read(cx).id();
|
||||||
|
@ -92,3 +57,60 @@ async fn test_channel_guests(
|
||||||
.await
|
.await
|
||||||
.is_err())
|
.is_err())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||||
|
let mut server = TestServer::start(cx_a.executor()).await;
|
||||||
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
let active_call_b = cx_b.read(ActiveCall::global);
|
||||||
|
|
||||||
|
let channel_id = server
|
||||||
|
.make_public_channel("the-channel", &client_a, cx_a)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project_a = client_a.build_test_project(cx_a).await;
|
||||||
|
cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client A shares a project in the channel
|
||||||
|
let project_id = active_call_a
|
||||||
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx_a.run_until_parked();
|
||||||
|
|
||||||
|
// Client B joins channel A as a guest
|
||||||
|
cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx_a.run_until_parked();
|
||||||
|
|
||||||
|
let project_b =
|
||||||
|
active_call_b.read_with(cx_b, |call, _| call.location().unwrap().upgrade().unwrap());
|
||||||
|
|
||||||
|
// client B opens 1.txt
|
||||||
|
let (workspace, cx_b) = client_b.active_workspace(cx_b);
|
||||||
|
cx_b.simulate_keystrokes("cmd-p 1 enter");
|
||||||
|
let editor_b = cx_b.update(|cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
|
||||||
|
|
||||||
|
active_call_a
|
||||||
|
.update(cx_a, |call, cx| {
|
||||||
|
call.room().unwrap().update(cx, |room, cx| {
|
||||||
|
room.set_participant_role(
|
||||||
|
client_b.user_id().unwrap(),
|
||||||
|
proto::ChannelRole::Member,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx_a.run_until_parked();
|
||||||
|
|
||||||
|
assert!(project_b.read_with(cx_b, |project, _| !project.is_read_only()));
|
||||||
|
assert!(!editor_b.update(cx_b, |e, cx| e.read_only(cx)));
|
||||||
|
}
|
||||||
|
|
|
@ -20,7 +20,11 @@ use node_runtime::FakeNodeRuntime;
|
||||||
use notifications::NotificationStore;
|
use notifications::NotificationStore;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{Project, WorktreeId};
|
use project::{Project, WorktreeId};
|
||||||
use rpc::{proto::ChannelRole, RECEIVE_TIMEOUT};
|
use rpc::{
|
||||||
|
proto::{self, ChannelRole},
|
||||||
|
RECEIVE_TIMEOUT,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Ref, RefCell, RefMut},
|
cell::{Ref, RefCell, RefMut},
|
||||||
|
@ -228,12 +232,16 @@ impl TestServer {
|
||||||
Project::init(&client, cx);
|
Project::init(&client, cx);
|
||||||
client::init(&client, cx);
|
client::init(&client, cx);
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
editor::init_settings(cx);
|
editor::init(cx);
|
||||||
workspace::init(app_state.clone(), cx);
|
workspace::init(app_state.clone(), cx);
|
||||||
audio::init((), cx);
|
audio::init((), cx);
|
||||||
call::init(client.clone(), user_store.clone(), cx);
|
call::init(client.clone(), user_store.clone(), cx);
|
||||||
channel::init(&client, user_store.clone(), cx);
|
channel::init(&client, user_store.clone(), cx);
|
||||||
notifications::init(client.clone(), user_store, cx);
|
notifications::init(client.clone(), user_store, cx);
|
||||||
|
collab_ui::init(&app_state, cx);
|
||||||
|
file_finder::init(cx);
|
||||||
|
menu::init();
|
||||||
|
settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
client
|
client
|
||||||
|
@ -351,6 +359,31 @@ impl TestServer {
|
||||||
channel_id
|
channel_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn make_public_channel(
|
||||||
|
&self,
|
||||||
|
channel: &str,
|
||||||
|
client: &TestClient,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) -> u64 {
|
||||||
|
let channel_id = self
|
||||||
|
.make_channel(channel, None, (client, cx), &mut [])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
client
|
||||||
|
.channel_store()
|
||||||
|
.update(cx, |channel_store, cx| {
|
||||||
|
channel_store.set_channel_visibility(
|
||||||
|
channel_id,
|
||||||
|
proto::ChannelVisibility::Public,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
channel_id
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn make_channel_tree(
|
pub async fn make_channel_tree(
|
||||||
&self,
|
&self,
|
||||||
channels: &[(&str, Option<&str>)],
|
channels: &[(&str, Option<&str>)],
|
||||||
|
@ -580,6 +613,20 @@ impl TestClient {
|
||||||
(project, worktree.read_with(cx, |tree, _| tree.id()))
|
(project, worktree.read_with(cx, |tree, _| tree.id()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn build_test_project(&self, cx: &mut TestAppContext) -> Model<Project> {
|
||||||
|
self.fs()
|
||||||
|
.insert_tree(
|
||||||
|
"/a",
|
||||||
|
json!({
|
||||||
|
"1.txt": "one\none\none",
|
||||||
|
"2.txt": "two\ntwo\ntwo",
|
||||||
|
"3.txt": "three\nthree\nthree",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
self.build_local_project("/a", cx).await.0
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Model<Project> {
|
pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Model<Project> {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
Project::local(
|
Project::local(
|
||||||
|
@ -619,6 +666,18 @@ impl TestClient {
|
||||||
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||||
cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn active_workspace<'a>(
|
||||||
|
&'a self,
|
||||||
|
cx: &'a mut TestAppContext,
|
||||||
|
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||||
|
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
|
||||||
|
|
||||||
|
let view = window.root_view(cx).unwrap();
|
||||||
|
let cx = Box::new(VisualTestContext::from_window(*window.deref(), cx));
|
||||||
|
// it might be nice to try and cleanup these at the end of each test.
|
||||||
|
(view, Box::leak(cx))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TestClient {
|
impl Drop for TestClient {
|
||||||
|
|
|
@ -37,7 +37,7 @@ use ui::{
|
||||||
use util::{maybe, ResultExt, TryFutureExt};
|
use util::{maybe, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
notifications::NotifyResultExt,
|
notifications::{NotifyResultExt, NotifyTaskExt},
|
||||||
Workspace,
|
Workspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -140,6 +140,7 @@ enum ListEntry {
|
||||||
user: Arc<User>,
|
user: Arc<User>,
|
||||||
peer_id: Option<PeerId>,
|
peer_id: Option<PeerId>,
|
||||||
is_pending: bool,
|
is_pending: bool,
|
||||||
|
role: proto::ChannelRole,
|
||||||
},
|
},
|
||||||
ParticipantProject {
|
ParticipantProject {
|
||||||
project_id: u64,
|
project_id: u64,
|
||||||
|
@ -151,10 +152,6 @@ enum ListEntry {
|
||||||
peer_id: Option<PeerId>,
|
peer_id: Option<PeerId>,
|
||||||
is_last: bool,
|
is_last: bool,
|
||||||
},
|
},
|
||||||
GuestCount {
|
|
||||||
count: usize,
|
|
||||||
has_visible_participants: bool,
|
|
||||||
},
|
|
||||||
IncomingRequest(Arc<User>),
|
IncomingRequest(Arc<User>),
|
||||||
OutgoingRequest(Arc<User>),
|
OutgoingRequest(Arc<User>),
|
||||||
ChannelInvite(Arc<Channel>),
|
ChannelInvite(Arc<Channel>),
|
||||||
|
@ -384,14 +381,10 @@ impl CollabPanel {
|
||||||
|
|
||||||
if !self.collapsed_sections.contains(&Section::ActiveCall) {
|
if !self.collapsed_sections.contains(&Section::ActiveCall) {
|
||||||
let room = room.read(cx);
|
let room = room.read(cx);
|
||||||
let mut guest_count_ix = 0;
|
|
||||||
let mut guest_count = if room.read_only() { 1 } else { 0 };
|
|
||||||
let mut non_guest_count = if room.read_only() { 0 } else { 1 };
|
|
||||||
|
|
||||||
if let Some(channel_id) = room.channel_id() {
|
if let Some(channel_id) = room.channel_id() {
|
||||||
self.entries.push(ListEntry::ChannelNotes { channel_id });
|
self.entries.push(ListEntry::ChannelNotes { channel_id });
|
||||||
self.entries.push(ListEntry::ChannelChat { channel_id });
|
self.entries.push(ListEntry::ChannelChat { channel_id });
|
||||||
guest_count_ix = self.entries.len();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate the active user.
|
// Populate the active user.
|
||||||
|
@ -410,12 +403,13 @@ impl CollabPanel {
|
||||||
&Default::default(),
|
&Default::default(),
|
||||||
executor.clone(),
|
executor.clone(),
|
||||||
));
|
));
|
||||||
if !matches.is_empty() && !room.read_only() {
|
if !matches.is_empty() {
|
||||||
let user_id = user.id;
|
let user_id = user.id;
|
||||||
self.entries.push(ListEntry::CallParticipant {
|
self.entries.push(ListEntry::CallParticipant {
|
||||||
user,
|
user,
|
||||||
peer_id: None,
|
peer_id: None,
|
||||||
is_pending: false,
|
is_pending: false,
|
||||||
|
role: room.local_participant().role,
|
||||||
});
|
});
|
||||||
let mut projects = room.local_participant().projects.iter().peekable();
|
let mut projects = room.local_participant().projects.iter().peekable();
|
||||||
while let Some(project) = projects.next() {
|
while let Some(project) = projects.next() {
|
||||||
|
@ -442,12 +436,6 @@ impl CollabPanel {
|
||||||
room.remote_participants()
|
room.remote_participants()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(_, participant)| {
|
.filter_map(|(_, participant)| {
|
||||||
if participant.role == proto::ChannelRole::Guest {
|
|
||||||
guest_count += 1;
|
|
||||||
return None;
|
|
||||||
} else {
|
|
||||||
non_guest_count += 1;
|
|
||||||
}
|
|
||||||
Some(StringMatchCandidate {
|
Some(StringMatchCandidate {
|
||||||
id: participant.user.id as usize,
|
id: participant.user.id as usize,
|
||||||
string: participant.user.github_login.clone(),
|
string: participant.user.github_login.clone(),
|
||||||
|
@ -455,7 +443,7 @@ impl CollabPanel {
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let matches = executor.block(match_strings(
|
let mut matches = executor.block(match_strings(
|
||||||
&self.match_candidates,
|
&self.match_candidates,
|
||||||
&query,
|
&query,
|
||||||
true,
|
true,
|
||||||
|
@ -463,6 +451,15 @@ impl CollabPanel {
|
||||||
&Default::default(),
|
&Default::default(),
|
||||||
executor.clone(),
|
executor.clone(),
|
||||||
));
|
));
|
||||||
|
matches.sort_by(|a, b| {
|
||||||
|
let a_is_guest = room.role_for_user(a.candidate_id as u64)
|
||||||
|
== Some(proto::ChannelRole::Guest);
|
||||||
|
let b_is_guest = room.role_for_user(b.candidate_id as u64)
|
||||||
|
== Some(proto::ChannelRole::Guest);
|
||||||
|
a_is_guest
|
||||||
|
.cmp(&b_is_guest)
|
||||||
|
.then_with(|| a.string.cmp(&b.string))
|
||||||
|
});
|
||||||
for mat in matches {
|
for mat in matches {
|
||||||
let user_id = mat.candidate_id as u64;
|
let user_id = mat.candidate_id as u64;
|
||||||
let participant = &room.remote_participants()[&user_id];
|
let participant = &room.remote_participants()[&user_id];
|
||||||
|
@ -470,6 +467,7 @@ impl CollabPanel {
|
||||||
user: participant.user.clone(),
|
user: participant.user.clone(),
|
||||||
peer_id: Some(participant.peer_id),
|
peer_id: Some(participant.peer_id),
|
||||||
is_pending: false,
|
is_pending: false,
|
||||||
|
role: participant.role,
|
||||||
});
|
});
|
||||||
let mut projects = participant.projects.iter().peekable();
|
let mut projects = participant.projects.iter().peekable();
|
||||||
while let Some(project) = projects.next() {
|
while let Some(project) = projects.next() {
|
||||||
|
@ -488,15 +486,6 @@ impl CollabPanel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if guest_count > 0 {
|
|
||||||
self.entries.insert(
|
|
||||||
guest_count_ix,
|
|
||||||
ListEntry::GuestCount {
|
|
||||||
count: guest_count,
|
|
||||||
has_visible_participants: non_guest_count > 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate pending participants.
|
// Populate pending participants.
|
||||||
self.match_candidates.clear();
|
self.match_candidates.clear();
|
||||||
|
@ -521,6 +510,7 @@ impl CollabPanel {
|
||||||
user: room.pending_participants()[mat.candidate_id].clone(),
|
user: room.pending_participants()[mat.candidate_id].clone(),
|
||||||
peer_id: None,
|
peer_id: None,
|
||||||
is_pending: true,
|
is_pending: true,
|
||||||
|
role: proto::ChannelRole::Member,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -834,13 +824,19 @@ impl CollabPanel {
|
||||||
user: &Arc<User>,
|
user: &Arc<User>,
|
||||||
peer_id: Option<PeerId>,
|
peer_id: Option<PeerId>,
|
||||||
is_pending: bool,
|
is_pending: bool,
|
||||||
|
role: proto::ChannelRole,
|
||||||
is_selected: bool,
|
is_selected: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> ListItem {
|
) -> ListItem {
|
||||||
|
let user_id = user.id;
|
||||||
let is_current_user =
|
let is_current_user =
|
||||||
self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
|
self.user_store.read(cx).current_user().map(|user| user.id) == Some(user_id);
|
||||||
let tooltip = format!("Follow {}", user.github_login);
|
let tooltip = format!("Follow {}", user.github_login);
|
||||||
|
|
||||||
|
let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| {
|
||||||
|
room.read(cx).local_participant().role == proto::ChannelRole::Admin
|
||||||
|
});
|
||||||
|
|
||||||
ListItem::new(SharedString::from(user.github_login.clone()))
|
ListItem::new(SharedString::from(user.github_login.clone()))
|
||||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||||
.child(Label::new(user.github_login.clone()))
|
.child(Label::new(user.github_login.clone()))
|
||||||
|
@ -853,17 +849,27 @@ impl CollabPanel {
|
||||||
.on_click(move |_, cx| Self::leave_call(cx))
|
.on_click(move |_, cx| Self::leave_call(cx))
|
||||||
.tooltip(|cx| Tooltip::text("Leave Call", cx))
|
.tooltip(|cx| Tooltip::text("Leave Call", cx))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
|
} else if role == proto::ChannelRole::Guest {
|
||||||
|
Label::new("Guest").color(Color::Muted).into_any_element()
|
||||||
} else {
|
} else {
|
||||||
div().into_any_element()
|
div().into_any_element()
|
||||||
})
|
})
|
||||||
.when_some(peer_id, |this, peer_id| {
|
.when_some(peer_id, |el, peer_id| {
|
||||||
this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
if role == proto::ChannelRole::Guest {
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
el.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.workspace
|
this.workspace
|
||||||
.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
|
.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
|
||||||
.ok();
|
.ok();
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
.when(is_call_admin && role == proto::ChannelRole::Guest, |el| {
|
||||||
|
el.on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| {
|
||||||
|
this.deploy_participant_context_menu(event.position, user_id, cx)
|
||||||
|
}))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_participant_project(
|
fn render_participant_project(
|
||||||
|
@ -986,41 +992,6 @@ impl CollabPanel {
|
||||||
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
|
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_guest_count(
|
|
||||||
&self,
|
|
||||||
count: usize,
|
|
||||||
has_visible_participants: bool,
|
|
||||||
is_selected: bool,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let manageable_channel_id = ActiveCall::global(cx).read(cx).room().and_then(|room| {
|
|
||||||
let room = room.read(cx);
|
|
||||||
if room.local_participant_is_admin() {
|
|
||||||
room.channel_id()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ListItem::new("guest_count")
|
|
||||||
.selected(is_selected)
|
|
||||||
.start_slot(
|
|
||||||
h_stack()
|
|
||||||
.gap_1()
|
|
||||||
.child(render_tree_branch(!has_visible_participants, false, cx))
|
|
||||||
.child(""),
|
|
||||||
)
|
|
||||||
.child(Label::new(if count == 1 {
|
|
||||||
format!("{} guest", count)
|
|
||||||
} else {
|
|
||||||
format!("{} guests", count)
|
|
||||||
}))
|
|
||||||
.when_some(manageable_channel_id, |el, channel_id| {
|
|
||||||
el.tooltip(move |cx| Tooltip::text("Manage Members", cx))
|
|
||||||
.on_click(cx.listener(move |this, _, cx| this.manage_members(channel_id, cx)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_subchannels(&self, ix: usize) -> bool {
|
fn has_subchannels(&self, ix: usize) -> bool {
|
||||||
self.entries.get(ix).map_or(false, |entry| {
|
self.entries.get(ix).map_or(false, |entry| {
|
||||||
if let ListEntry::Channel { has_children, .. } = entry {
|
if let ListEntry::Channel { has_children, .. } = entry {
|
||||||
|
@ -1031,6 +1002,47 @@ impl CollabPanel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deploy_participant_context_menu(
|
||||||
|
&mut self,
|
||||||
|
position: Point<Pixels>,
|
||||||
|
user_id: u64,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let this = cx.view().clone();
|
||||||
|
|
||||||
|
let context_menu = ContextMenu::build(cx, |context_menu, cx| {
|
||||||
|
context_menu.entry(
|
||||||
|
"Allow Write Access",
|
||||||
|
None,
|
||||||
|
cx.handler_for(&this, move |_, cx| {
|
||||||
|
ActiveCall::global(cx)
|
||||||
|
.update(cx, |call, cx| {
|
||||||
|
let Some(room) = call.room() else {
|
||||||
|
return Task::ready(Ok(()));
|
||||||
|
};
|
||||||
|
room.update(cx, |room, cx| {
|
||||||
|
room.set_participant_role(user_id, proto::ChannelRole::Member, cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_notify_err(cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.focus_view(&context_menu);
|
||||||
|
let subscription =
|
||||||
|
cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
|
||||||
|
if this.context_menu.as_ref().is_some_and(|context_menu| {
|
||||||
|
context_menu.0.focus_handle(cx).contains_focused(cx)
|
||||||
|
}) {
|
||||||
|
cx.focus_self();
|
||||||
|
}
|
||||||
|
this.context_menu.take();
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
self.context_menu = Some((context_menu, position, subscription));
|
||||||
|
}
|
||||||
|
|
||||||
fn deploy_channel_context_menu(
|
fn deploy_channel_context_menu(
|
||||||
&mut self,
|
&mut self,
|
||||||
position: Point<Pixels>,
|
position: Point<Pixels>,
|
||||||
|
@ -1242,18 +1254,6 @@ impl CollabPanel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ListEntry::GuestCount { .. } => {
|
|
||||||
let Some(room) = ActiveCall::global(cx).read(cx).room() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let room = room.read(cx);
|
|
||||||
let Some(channel_id) = room.channel_id() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if room.local_participant_is_admin() {
|
|
||||||
self.manage_members(channel_id, cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListEntry::Channel { channel, .. } => {
|
ListEntry::Channel { channel, .. } => {
|
||||||
let is_active = maybe!({
|
let is_active = maybe!({
|
||||||
let call_channel = ActiveCall::global(cx)
|
let call_channel = ActiveCall::global(cx)
|
||||||
|
@ -1788,8 +1788,9 @@ impl CollabPanel {
|
||||||
user,
|
user,
|
||||||
peer_id,
|
peer_id,
|
||||||
is_pending,
|
is_pending,
|
||||||
|
role,
|
||||||
} => self
|
} => self
|
||||||
.render_call_participant(user, *peer_id, *is_pending, is_selected, cx)
|
.render_call_participant(user, *peer_id, *is_pending, *role, is_selected, cx)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
ListEntry::ParticipantProject {
|
ListEntry::ParticipantProject {
|
||||||
project_id,
|
project_id,
|
||||||
|
@ -1809,12 +1810,6 @@ impl CollabPanel {
|
||||||
ListEntry::ParticipantScreen { peer_id, is_last } => self
|
ListEntry::ParticipantScreen { peer_id, is_last } => self
|
||||||
.render_participant_screen(*peer_id, *is_last, is_selected, cx)
|
.render_participant_screen(*peer_id, *is_last, is_selected, cx)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
ListEntry::GuestCount {
|
|
||||||
count,
|
|
||||||
has_visible_participants,
|
|
||||||
} => self
|
|
||||||
.render_guest_count(*count, *has_visible_participants, is_selected, cx)
|
|
||||||
.into_any_element(),
|
|
||||||
ListEntry::ChannelNotes { channel_id } => self
|
ListEntry::ChannelNotes { channel_id } => self
|
||||||
.render_channel_notes(*channel_id, is_selected, cx)
|
.render_channel_notes(*channel_id, is_selected, cx)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
|
@ -2621,11 +2616,6 @@ impl PartialEq for ListEntry {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ListEntry::GuestCount { .. } => {
|
|
||||||
if let ListEntry::GuestCount { .. } = other {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
|
@ -238,6 +238,10 @@ impl TestAppContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn run_until_parked(&mut self) {
|
||||||
|
self.background_executor.run_until_parked()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dispatch_action<A>(&mut self, window: AnyWindowHandle, action: A)
|
pub fn dispatch_action<A>(&mut self, window: AnyWindowHandle, action: A)
|
||||||
where
|
where
|
||||||
A: Action,
|
A: Action,
|
||||||
|
|
|
@ -254,6 +254,7 @@ pub enum Event {
|
||||||
LanguageChanged,
|
LanguageChanged,
|
||||||
Reparsed,
|
Reparsed,
|
||||||
DiagnosticsUpdated,
|
DiagnosticsUpdated,
|
||||||
|
CapabilityChanged,
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -631,6 +632,11 @@ impl Buffer {
|
||||||
.set_language_registry(language_registry);
|
.set_language_registry(language_registry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_capability(&mut self, capability: Capability, cx: &mut ModelContext<Self>) {
|
||||||
|
self.capability = capability;
|
||||||
|
cx.emit(Event::CapabilityChanged)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn did_save(
|
pub fn did_save(
|
||||||
&mut self,
|
&mut self,
|
||||||
version: clock::Global,
|
version: clock::Global,
|
||||||
|
|
|
@ -80,6 +80,7 @@ pub enum Event {
|
||||||
Reloaded,
|
Reloaded,
|
||||||
DiffBaseChanged,
|
DiffBaseChanged,
|
||||||
LanguageChanged,
|
LanguageChanged,
|
||||||
|
CapabilityChanged,
|
||||||
Reparsed,
|
Reparsed,
|
||||||
Saved,
|
Saved,
|
||||||
FileHandleChanged,
|
FileHandleChanged,
|
||||||
|
@ -1404,7 +1405,7 @@ impl MultiBuffer {
|
||||||
|
|
||||||
fn on_buffer_event(
|
fn on_buffer_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Model<Buffer>,
|
buffer: Model<Buffer>,
|
||||||
event: &language::Event,
|
event: &language::Event,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
|
@ -1421,6 +1422,10 @@ impl MultiBuffer {
|
||||||
language::Event::Reparsed => Event::Reparsed,
|
language::Event::Reparsed => Event::Reparsed,
|
||||||
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
|
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
|
||||||
language::Event::Closed => Event::Closed,
|
language::Event::Closed => Event::Closed,
|
||||||
|
language::Event::CapabilityChanged => {
|
||||||
|
self.capability = buffer.read(cx).capability();
|
||||||
|
Event::CapabilityChanged
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
language::Event::Operation(_) => return,
|
language::Event::Operation(_) => return,
|
||||||
|
|
|
@ -799,7 +799,7 @@ impl Project {
|
||||||
prettiers_per_worktree: HashMap::default(),
|
prettiers_per_worktree: HashMap::default(),
|
||||||
prettier_instances: HashMap::default(),
|
prettier_instances: HashMap::default(),
|
||||||
};
|
};
|
||||||
this.set_role(role);
|
this.set_role(role, cx);
|
||||||
for worktree in worktrees {
|
for worktree in worktrees {
|
||||||
let _ = this.add_worktree(&worktree, cx);
|
let _ = this.add_worktree(&worktree, cx);
|
||||||
}
|
}
|
||||||
|
@ -1622,14 +1622,22 @@ impl Project {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_role(&mut self, role: proto::ChannelRole) {
|
pub fn set_role(&mut self, role: proto::ChannelRole, cx: &mut ModelContext<Self>) {
|
||||||
if let Some(ProjectClientState::Remote { capability, .. }) = &mut self.client_state {
|
let new_capability =
|
||||||
*capability = if role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin
|
if role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin {
|
||||||
{
|
|
||||||
Capability::ReadWrite
|
Capability::ReadWrite
|
||||||
} else {
|
} else {
|
||||||
Capability::ReadOnly
|
Capability::ReadOnly
|
||||||
};
|
};
|
||||||
|
if let Some(ProjectClientState::Remote { capability, .. }) = &mut self.client_state {
|
||||||
|
if *capability == new_capability {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
*capability = new_capability;
|
||||||
|
}
|
||||||
|
for buffer in self.opened_buffers() {
|
||||||
|
buffer.update(cx, |buffer, cx| buffer.set_capability(new_capability, cx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -180,7 +180,8 @@ message Envelope {
|
||||||
DeleteNotification delete_notification = 152;
|
DeleteNotification delete_notification = 152;
|
||||||
MarkNotificationRead mark_notification_read = 153;
|
MarkNotificationRead mark_notification_read = 153;
|
||||||
LspExtExpandMacro lsp_ext_expand_macro = 154;
|
LspExtExpandMacro lsp_ext_expand_macro = 154;
|
||||||
LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155; // Current max
|
LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155;
|
||||||
|
SetRoomParticipantRole set_room_participant_role = 156; // Current max
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1633,3 +1634,9 @@ message LspExtExpandMacroResponse {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
string expansion = 2;
|
string expansion = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SetRoomParticipantRole {
|
||||||
|
uint64 room_id = 1;
|
||||||
|
uint64 user_id = 2;
|
||||||
|
ChannelRole role = 3;
|
||||||
|
}
|
||||||
|
|
|
@ -283,6 +283,7 @@ messages!(
|
||||||
(UsersResponse, Foreground),
|
(UsersResponse, Foreground),
|
||||||
(LspExtExpandMacro, Background),
|
(LspExtExpandMacro, Background),
|
||||||
(LspExtExpandMacroResponse, Background),
|
(LspExtExpandMacroResponse, Background),
|
||||||
|
(SetRoomParticipantRole, Foreground),
|
||||||
);
|
);
|
||||||
|
|
||||||
request_messages!(
|
request_messages!(
|
||||||
|
@ -367,6 +368,7 @@ request_messages!(
|
||||||
(UpdateProject, Ack),
|
(UpdateProject, Ack),
|
||||||
(UpdateWorktree, Ack),
|
(UpdateWorktree, Ack),
|
||||||
(LspExtExpandMacro, LspExtExpandMacroResponse),
|
(LspExtExpandMacro, LspExtExpandMacroResponse),
|
||||||
|
(SetRoomParticipantRole, Ack),
|
||||||
);
|
);
|
||||||
|
|
||||||
entity_messages!(
|
entity_messages!(
|
||||||
|
|
|
@ -17,7 +17,6 @@ pub struct VimTestContext {
|
||||||
impl VimTestContext {
|
impl VimTestContext {
|
||||||
pub fn init(cx: &mut gpui::TestAppContext) {
|
pub fn init(cx: &mut gpui::TestAppContext) {
|
||||||
if cx.has_global::<Vim>() {
|
if cx.has_global::<Vim>() {
|
||||||
dbg!("OOPS");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::{Toast, Workspace};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
|
AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
|
||||||
View, ViewContext, VisualContext,
|
Task, View, ViewContext, VisualContext, WindowContext,
|
||||||
};
|
};
|
||||||
use std::{any::TypeId, ops::DerefMut};
|
use std::{any::TypeId, ops::DerefMut};
|
||||||
|
|
||||||
|
@ -393,3 +393,18 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait NotifyTaskExt {
|
||||||
|
fn detach_and_notify_err(self, cx: &mut WindowContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R, E> NotifyTaskExt for Task<Result<R, E>>
|
||||||
|
where
|
||||||
|
E: std::fmt::Debug + 'static,
|
||||||
|
R: 'static,
|
||||||
|
{
|
||||||
|
fn detach_and_notify_err(self, cx: &mut WindowContext) {
|
||||||
|
cx.spawn(|mut cx| async move { self.await.notify_async_err(&mut cx) })
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue