Read-only access for channel guests (#3841)
This commit is contained in:
commit
1a993ca496
42 changed files with 619 additions and 191 deletions
|
@ -2827,8 +2827,8 @@ impl InlineAssistant {
|
||||||
|
|
||||||
fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
|
fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
|
||||||
let is_read_only = !self.codegen.read(cx).idle();
|
let is_read_only = !self.codegen.read(cx).idle();
|
||||||
self.prompt_editor.update(cx, |editor, _cx| {
|
self.prompt_editor.update(cx, |editor, cx| {
|
||||||
let was_read_only = editor.read_only();
|
let was_read_only = editor.read_only(cx);
|
||||||
if was_read_only != is_read_only {
|
if was_read_only != is_read_only {
|
||||||
if is_read_only {
|
if is_read_only {
|
||||||
editor.set_read_only(true);
|
editor.set_read_only(true);
|
||||||
|
@ -3063,7 +3063,7 @@ impl InlineAssistant {
|
||||||
fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let settings = ThemeSettings::get_global(cx);
|
let settings = ThemeSettings::get_global(cx);
|
||||||
let text_style = TextStyle {
|
let text_style = TextStyle {
|
||||||
color: if self.prompt_editor.read(cx).read_only() {
|
color: if self.prompt_editor.read(cx).read_only(cx) {
|
||||||
cx.theme().colors().text_disabled
|
cx.theme().colors().text_disabled
|
||||||
} else {
|
} else {
|
||||||
cx.theme().colors().text
|
cx.theme().colors().text
|
||||||
|
|
|
@ -36,12 +36,14 @@ impl ParticipantLocation {
|
||||||
pub struct LocalParticipant {
|
pub struct LocalParticipant {
|
||||||
pub projects: Vec<proto::ParticipantProject>,
|
pub projects: Vec<proto::ParticipantProject>,
|
||||||
pub active_project: Option<WeakModel<Project>>,
|
pub active_project: Option<WeakModel<Project>>,
|
||||||
|
pub role: proto::ChannelRole,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct RemoteParticipant {
|
pub struct RemoteParticipant {
|
||||||
pub user: Arc<User>,
|
pub user: Arc<User>,
|
||||||
pub peer_id: proto::PeerId,
|
pub peer_id: proto::PeerId,
|
||||||
|
pub role: proto::ChannelRole,
|
||||||
pub projects: Vec<proto::ParticipantProject>,
|
pub projects: Vec<proto::ParticipantProject>,
|
||||||
pub location: ParticipantLocation,
|
pub location: ParticipantLocation,
|
||||||
pub participant_index: ParticipantIndex,
|
pub participant_index: ParticipantIndex,
|
||||||
|
|
|
@ -247,14 +247,18 @@ impl Room {
|
||||||
let response = client.request(proto::CreateRoom {}).await?;
|
let response = client.request(proto::CreateRoom {}).await?;
|
||||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||||
let room = cx.new_model(|cx| {
|
let room = cx.new_model(|cx| {
|
||||||
Self::new(
|
let mut room = Self::new(
|
||||||
room_proto.id,
|
room_proto.id,
|
||||||
None,
|
None,
|
||||||
response.live_kit_connection_info,
|
response.live_kit_connection_info,
|
||||||
client,
|
client,
|
||||||
user_store,
|
user_store,
|
||||||
cx,
|
cx,
|
||||||
)
|
);
|
||||||
|
if let Some(participant) = room_proto.participants.first() {
|
||||||
|
room.local_participant.role = participant.role()
|
||||||
|
}
|
||||||
|
room
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||||
|
@ -606,6 +610,16 @@ impl Room {
|
||||||
.find(|p| p.peer_id == peer_id)
|
.find(|p| p.peer_id == peer_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn role_for_user(&self, user_id: u64) -> Option<proto::ChannelRole> {
|
||||||
|
self.remote_participants
|
||||||
|
.get(&user_id)
|
||||||
|
.map(|participant| participant.role)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local_participant_is_admin(&self) -> bool {
|
||||||
|
self.local_participant.role == proto::ChannelRole::Admin
|
||||||
|
}
|
||||||
|
|
||||||
pub fn pending_participants(&self) -> &[Arc<User>] {
|
pub fn pending_participants(&self) -> &[Arc<User>] {
|
||||||
&self.pending_participants
|
&self.pending_participants
|
||||||
}
|
}
|
||||||
|
@ -710,7 +724,20 @@ impl Room {
|
||||||
this.participant_user_ids.clear();
|
this.participant_user_ids.clear();
|
||||||
|
|
||||||
if let Some(participant) = local_participant {
|
if let Some(participant) = local_participant {
|
||||||
|
let role = participant.role();
|
||||||
this.local_participant.projects = participant.projects;
|
this.local_participant.projects = participant.projects;
|
||||||
|
if this.local_participant.role != role {
|
||||||
|
this.local_participant.role = role;
|
||||||
|
|
||||||
|
this.joined_projects.retain(|project| {
|
||||||
|
if let Some(project) = project.upgrade() {
|
||||||
|
project.update(cx, |project, _| project.set_role(role));
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.local_participant.projects.clear();
|
this.local_participant.projects.clear();
|
||||||
}
|
}
|
||||||
|
@ -766,6 +793,7 @@ impl Room {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let role = participant.role();
|
||||||
let location = ParticipantLocation::from_proto(participant.location)
|
let location = ParticipantLocation::from_proto(participant.location)
|
||||||
.unwrap_or(ParticipantLocation::External);
|
.unwrap_or(ParticipantLocation::External);
|
||||||
if let Some(remote_participant) =
|
if let Some(remote_participant) =
|
||||||
|
@ -774,8 +802,11 @@ impl Room {
|
||||||
remote_participant.peer_id = peer_id;
|
remote_participant.peer_id = peer_id;
|
||||||
remote_participant.projects = participant.projects;
|
remote_participant.projects = participant.projects;
|
||||||
remote_participant.participant_index = participant_index;
|
remote_participant.participant_index = participant_index;
|
||||||
if location != remote_participant.location {
|
if location != remote_participant.location
|
||||||
|
|| role != remote_participant.role
|
||||||
|
{
|
||||||
remote_participant.location = location;
|
remote_participant.location = location;
|
||||||
|
remote_participant.role = role;
|
||||||
cx.emit(Event::ParticipantLocationChanged {
|
cx.emit(Event::ParticipantLocationChanged {
|
||||||
participant_id: peer_id,
|
participant_id: peer_id,
|
||||||
});
|
});
|
||||||
|
@ -789,6 +820,7 @@ impl Room {
|
||||||
peer_id,
|
peer_id,
|
||||||
projects: participant.projects,
|
projects: participant.projects,
|
||||||
location,
|
location,
|
||||||
|
role,
|
||||||
muted: true,
|
muted: true,
|
||||||
speaking: false,
|
speaking: false,
|
||||||
video_tracks: Default::default(),
|
video_tracks: Default::default(),
|
||||||
|
@ -1091,15 +1123,24 @@ impl Room {
|
||||||
) -> Task<Result<Model<Project>>> {
|
) -> Task<Result<Model<Project>>> {
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
let user_store = self.user_store.clone();
|
let user_store = self.user_store.clone();
|
||||||
|
let role = self.local_participant.role;
|
||||||
cx.emit(Event::RemoteProjectJoined { project_id: id });
|
cx.emit(Event::RemoteProjectJoined { project_id: id });
|
||||||
cx.spawn(move |this, mut cx| async move {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
let project =
|
let project = Project::remote(
|
||||||
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
|
id,
|
||||||
|
client,
|
||||||
|
user_store,
|
||||||
|
language_registry,
|
||||||
|
fs,
|
||||||
|
role,
|
||||||
|
cx.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.joined_projects.retain(|project| {
|
this.joined_projects.retain(|project| {
|
||||||
if let Some(project) = project.upgrade() {
|
if let Some(project) = project.upgrade() {
|
||||||
!project.read(cx).is_read_only()
|
!project.read(cx).is_disconnected()
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -1224,6 +1265,11 @@ impl Room {
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_only(&self) -> bool {
|
||||||
|
!(self.local_participant().role == proto::ChannelRole::Member
|
||||||
|
|| self.local_participant().role == proto::ChannelRole::Admin)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_speaking(&self) -> bool {
|
pub fn is_speaking(&self) -> bool {
|
||||||
self.live_kit
|
self.live_kit
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
|
@ -62,7 +62,12 @@ impl ChannelBuffer {
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
let buffer = cx.new_model(|_| {
|
let buffer = cx.new_model(|_| {
|
||||||
language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text)
|
language::Buffer::remote(
|
||||||
|
response.buffer_id,
|
||||||
|
response.replica_id as u16,
|
||||||
|
channel.channel_buffer_capability(),
|
||||||
|
base_text,
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))??;
|
buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))??;
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ use gpui::{
|
||||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task,
|
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task,
|
||||||
WeakModel,
|
WeakModel,
|
||||||
};
|
};
|
||||||
|
use language::Capability;
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{self, ChannelVisibility},
|
proto::{self, ChannelVisibility},
|
||||||
TypedEnvelope,
|
TypedEnvelope,
|
||||||
|
@ -74,8 +75,12 @@ impl Channel {
|
||||||
slug.trim_matches(|c| c == '-').to_string()
|
slug.trim_matches(|c| c == '-').to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_edit_notes(&self) -> bool {
|
pub fn channel_buffer_capability(&self) -> Capability {
|
||||||
self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin
|
if self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin {
|
||||||
|
Capability::ReadWrite
|
||||||
|
} else {
|
||||||
|
Capability::ReadOnly
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -161,7 +161,8 @@ CREATE TABLE "room_participants" (
|
||||||
"calling_user_id" INTEGER NOT NULL REFERENCES users (id),
|
"calling_user_id" INTEGER NOT NULL REFERENCES users (id),
|
||||||
"calling_connection_id" INTEGER NOT NULL,
|
"calling_connection_id" INTEGER NOT NULL,
|
||||||
"calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL,
|
"calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL,
|
||||||
"participant_index" INTEGER
|
"participant_index" INTEGER,
|
||||||
|
"role" TEXT
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
|
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
|
||||||
CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");
|
CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE room_participants ADD COLUMN role TEXT;
|
|
@ -132,6 +132,14 @@ impl ChannelRole {
|
||||||
Admin | Member | Banned => false,
|
Admin | Member | Banned => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn can_share_projects(&self) -> bool {
|
||||||
|
use ChannelRole::*;
|
||||||
|
match self {
|
||||||
|
Admin | Member => true,
|
||||||
|
Guest | Banned => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<proto::ChannelRole> for ChannelRole {
|
impl From<proto::ChannelRole> for ChannelRole {
|
||||||
|
|
|
@ -132,10 +132,7 @@ impl Database {
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
self.channel_role_for_user(&channel, user_id, &*tx).await? == role
|
self.channel_role_for_user(&channel, user_id, &*tx).await? == role
|
||||||
);
|
);
|
||||||
}
|
} else if channel.visibility == ChannelVisibility::Public {
|
||||||
}
|
|
||||||
|
|
||||||
if channel.visibility == ChannelVisibility::Public {
|
|
||||||
role = Some(ChannelRole::Guest);
|
role = Some(ChannelRole::Guest);
|
||||||
let channel_to_join = self
|
let channel_to_join = self
|
||||||
.public_ancestors_including_self(&channel, &*tx)
|
.public_ancestors_including_self(&channel, &*tx)
|
||||||
|
@ -159,21 +156,25 @@ impl Database {
|
||||||
.await?,
|
.await?,
|
||||||
);
|
);
|
||||||
|
|
||||||
debug_assert!(self.channel_role_for_user(&channel, user_id, &*tx).await? == role);
|
debug_assert!(
|
||||||
|
self.channel_role_for_user(&channel, user_id, &*tx).await? == role
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if role.is_none() || role == Some(ChannelRole::Banned) {
|
if role.is_none() || role == Some(ChannelRole::Banned) {
|
||||||
Err(anyhow!("not allowed"))?
|
Err(anyhow!("not allowed"))?
|
||||||
}
|
}
|
||||||
|
let role = role.unwrap();
|
||||||
|
|
||||||
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
|
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
|
||||||
let room_id = self
|
let room_id = self
|
||||||
.get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx)
|
.get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.join_channel_room_internal(room_id, user_id, connection, &*tx)
|
self.join_channel_room_internal(room_id, user_id, connection, role, &*tx)
|
||||||
.await
|
.await
|
||||||
.map(|jr| (jr, accept_invite_result, role.unwrap()))
|
.map(|jr| (jr, accept_invite_result, role))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,13 @@ impl Database {
|
||||||
if participant.room_id != room_id {
|
if participant.room_id != room_id {
|
||||||
return Err(anyhow!("shared project on unexpected room"))?;
|
return Err(anyhow!("shared project on unexpected room"))?;
|
||||||
}
|
}
|
||||||
|
if !participant
|
||||||
|
.role
|
||||||
|
.unwrap_or(ChannelRole::Member)
|
||||||
|
.can_share_projects()
|
||||||
|
{
|
||||||
|
return Err(anyhow!("guests cannot share projects"))?;
|
||||||
|
}
|
||||||
|
|
||||||
let project = project::ActiveModel {
|
let project = project::ActiveModel {
|
||||||
room_id: ActiveValue::set(participant.room_id),
|
room_id: ActiveValue::set(participant.room_id),
|
||||||
|
|
|
@ -131,7 +131,12 @@ impl Database {
|
||||||
connection.owner_id as i32,
|
connection.owner_id as i32,
|
||||||
))),
|
))),
|
||||||
participant_index: ActiveValue::set(Some(0)),
|
participant_index: ActiveValue::set(Some(0)),
|
||||||
..Default::default()
|
role: ActiveValue::set(Some(ChannelRole::Admin)),
|
||||||
|
|
||||||
|
id: ActiveValue::NotSet,
|
||||||
|
location_kind: ActiveValue::NotSet,
|
||||||
|
location_project_id: ActiveValue::NotSet,
|
||||||
|
initial_project_id: ActiveValue::NotSet,
|
||||||
}
|
}
|
||||||
.insert(&*tx)
|
.insert(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -151,6 +156,22 @@ impl Database {
|
||||||
initial_project_id: Option<ProjectId>,
|
initial_project_id: Option<ProjectId>,
|
||||||
) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
|
) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
|
let caller = room_participant::Entity::find()
|
||||||
|
.filter(
|
||||||
|
room_participant::Column::UserId
|
||||||
|
.eq(calling_user_id)
|
||||||
|
.and(room_participant::Column::RoomId.eq(room_id)),
|
||||||
|
)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("user is not in the room"))?;
|
||||||
|
|
||||||
|
let called_user_role = match caller.role.unwrap_or(ChannelRole::Member) {
|
||||||
|
ChannelRole::Admin | ChannelRole::Member => ChannelRole::Member,
|
||||||
|
ChannelRole::Guest => ChannelRole::Guest,
|
||||||
|
ChannelRole::Banned => return Err(anyhow!("banned users cannot invite").into()),
|
||||||
|
};
|
||||||
|
|
||||||
room_participant::ActiveModel {
|
room_participant::ActiveModel {
|
||||||
room_id: ActiveValue::set(room_id),
|
room_id: ActiveValue::set(room_id),
|
||||||
user_id: ActiveValue::set(called_user_id),
|
user_id: ActiveValue::set(called_user_id),
|
||||||
|
@ -162,7 +183,13 @@ impl Database {
|
||||||
calling_connection.owner_id as i32,
|
calling_connection.owner_id as i32,
|
||||||
))),
|
))),
|
||||||
initial_project_id: ActiveValue::set(initial_project_id),
|
initial_project_id: ActiveValue::set(initial_project_id),
|
||||||
..Default::default()
|
role: ActiveValue::set(Some(called_user_role)),
|
||||||
|
|
||||||
|
id: ActiveValue::NotSet,
|
||||||
|
answering_connection_id: ActiveValue::NotSet,
|
||||||
|
answering_connection_server_id: ActiveValue::NotSet,
|
||||||
|
location_kind: ActiveValue::NotSet,
|
||||||
|
location_project_id: ActiveValue::NotSet,
|
||||||
}
|
}
|
||||||
.insert(&*tx)
|
.insert(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -384,6 +411,7 @@ impl Database {
|
||||||
room_id: RoomId,
|
room_id: RoomId,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
|
role: ChannelRole,
|
||||||
tx: &DatabaseTransaction,
|
tx: &DatabaseTransaction,
|
||||||
) -> Result<JoinRoom> {
|
) -> Result<JoinRoom> {
|
||||||
let participant_index = self
|
let participant_index = self
|
||||||
|
@ -404,7 +432,11 @@ impl Database {
|
||||||
connection.owner_id as i32,
|
connection.owner_id as i32,
|
||||||
))),
|
))),
|
||||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||||
..Default::default()
|
role: ActiveValue::set(Some(role)),
|
||||||
|
id: ActiveValue::NotSet,
|
||||||
|
location_kind: ActiveValue::NotSet,
|
||||||
|
location_project_id: ActiveValue::NotSet,
|
||||||
|
initial_project_id: ActiveValue::NotSet,
|
||||||
}])
|
}])
|
||||||
.on_conflict(
|
.on_conflict(
|
||||||
OnConflict::columns([room_participant::Column::UserId])
|
OnConflict::columns([room_participant::Column::UserId])
|
||||||
|
@ -413,6 +445,7 @@ impl Database {
|
||||||
room_participant::Column::AnsweringConnectionServerId,
|
room_participant::Column::AnsweringConnectionServerId,
|
||||||
room_participant::Column::AnsweringConnectionLost,
|
room_participant::Column::AnsweringConnectionLost,
|
||||||
room_participant::Column::ParticipantIndex,
|
room_participant::Column::ParticipantIndex,
|
||||||
|
room_participant::Column::Role,
|
||||||
])
|
])
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
|
@ -1126,6 +1159,7 @@ impl Database {
|
||||||
projects: Default::default(),
|
projects: Default::default(),
|
||||||
location: Some(proto::ParticipantLocation { variant: location }),
|
location: Some(proto::ParticipantLocation { variant: location }),
|
||||||
participant_index: participant_index as u32,
|
participant_index: participant_index as u32,
|
||||||
|
role: db_participant.role.unwrap_or(ChannelRole::Member).into(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
|
use crate::db::{ChannelRole, ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
|
||||||
use rpc::ConnectionId;
|
use rpc::ConnectionId;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ pub struct Model {
|
||||||
pub calling_connection_id: i32,
|
pub calling_connection_id: i32,
|
||||||
pub calling_connection_server_id: Option<ServerId>,
|
pub calling_connection_server_id: Option<ServerId>,
|
||||||
pub participant_index: Option<i32>,
|
pub participant_index: Option<i32>,
|
||||||
|
pub role: Option<ChannelRole>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
|
|
|
@ -2,6 +2,7 @@ use call::Room;
|
||||||
use gpui::{Model, TestAppContext};
|
use gpui::{Model, TestAppContext};
|
||||||
|
|
||||||
mod channel_buffer_tests;
|
mod channel_buffer_tests;
|
||||||
|
mod channel_guest_tests;
|
||||||
mod channel_message_tests;
|
mod channel_message_tests;
|
||||||
mod channel_tests;
|
mod channel_tests;
|
||||||
mod editor_tests;
|
mod editor_tests;
|
||||||
|
|
86
crates/collab/src/tests/channel_guest_tests.rs
Normal file
86
crates/collab/src/tests/channel_guest_tests.rs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
use crate::tests::TestServer;
|
||||||
|
use call::ActiveCall;
|
||||||
|
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||||
|
use rpc::proto;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_channel_guests(
|
||||||
|
executor: BackgroundExecutor,
|
||||||
|
cx_a: &mut TestAppContext,
|
||||||
|
cx_b: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let mut server = TestServer::start(executor.clone()).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_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);
|
||||||
|
|
||||||
|
// Client A shares a project in the channel
|
||||||
|
active_call_a
|
||||||
|
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||||
|
let project_id = active_call_a
|
||||||
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx_a.executor().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();
|
||||||
|
|
||||||
|
// b should be following a in the shared project.
|
||||||
|
// B is a guest,
|
||||||
|
cx_a.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());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
project_b.read_with(cx_b, |project, _| project.remote_id()),
|
||||||
|
Some(project_id),
|
||||||
|
);
|
||||||
|
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()))
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ use project::{
|
||||||
search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath,
|
search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath,
|
||||||
};
|
};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
|
use rpc::proto::ChannelRole;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -1380,7 +1381,7 @@ async fn test_unshare_project(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
|
|
||||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
assert!(project_b.read_with(cx_b, |project, _| project.is_disconnected()));
|
||||||
|
|
||||||
// Client C opens the project.
|
// Client C opens the project.
|
||||||
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||||
|
@ -1393,7 +1394,7 @@ async fn test_unshare_project(
|
||||||
|
|
||||||
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
|
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
|
||||||
|
|
||||||
assert!(project_c.read_with(cx_c, |project, _| project.is_read_only()));
|
assert!(project_c.read_with(cx_c, |project, _| project.is_disconnected()));
|
||||||
|
|
||||||
// Client C can open the project again after client A re-shares.
|
// Client C can open the project again after client A re-shares.
|
||||||
let project_id = active_call_a
|
let project_id = active_call_a
|
||||||
|
@ -1419,7 +1420,7 @@ async fn test_unshare_project(
|
||||||
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
|
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
|
||||||
|
|
||||||
project_c2.read_with(cx_c, |project, _| {
|
project_c2.read_with(cx_c, |project, _| {
|
||||||
assert!(project.is_read_only());
|
assert!(project.is_disconnected());
|
||||||
assert!(project.collaborators().is_empty());
|
assert!(project.collaborators().is_empty());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1551,7 +1552,7 @@ async fn test_project_reconnect(
|
||||||
});
|
});
|
||||||
|
|
||||||
project_b1.read_with(cx_b, |project, _| {
|
project_b1.read_with(cx_b, |project, _| {
|
||||||
assert!(!project.is_read_only());
|
assert!(!project.is_disconnected());
|
||||||
assert_eq!(project.collaborators().len(), 1);
|
assert_eq!(project.collaborators().len(), 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1653,7 +1654,7 @@ async fn test_project_reconnect(
|
||||||
});
|
});
|
||||||
|
|
||||||
project_b1.read_with(cx_b, |project, cx| {
|
project_b1.read_with(cx_b, |project, cx| {
|
||||||
assert!(!project.is_read_only());
|
assert!(!project.is_disconnected());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
project
|
project
|
||||||
.worktree_for_id(worktree1_id, cx)
|
.worktree_for_id(worktree1_id, cx)
|
||||||
|
@ -1687,9 +1688,9 @@ async fn test_project_reconnect(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
project_b2.read_with(cx_b, |project, _| assert!(project.is_read_only()));
|
project_b2.read_with(cx_b, |project, _| assert!(project.is_disconnected()));
|
||||||
|
|
||||||
project_b3.read_with(cx_b, |project, _| assert!(!project.is_read_only()));
|
project_b3.read_with(cx_b, |project, _| assert!(!project.is_disconnected()));
|
||||||
|
|
||||||
buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
|
buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
|
||||||
|
|
||||||
|
@ -1746,7 +1747,7 @@ async fn test_project_reconnect(
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
|
|
||||||
project_b1.read_with(cx_b, |project, cx| {
|
project_b1.read_with(cx_b, |project, cx| {
|
||||||
assert!(!project.is_read_only());
|
assert!(!project.is_disconnected());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
project
|
project
|
||||||
.worktree_for_id(worktree1_id, cx)
|
.worktree_for_id(worktree1_id, cx)
|
||||||
|
@ -1780,7 +1781,7 @@ async fn test_project_reconnect(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
project_b3.read_with(cx_b, |project, _| assert!(project.is_read_only()));
|
project_b3.read_with(cx_b, |project, _| assert!(project.is_disconnected()));
|
||||||
|
|
||||||
buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
|
buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
|
||||||
|
|
||||||
|
@ -3535,7 +3536,7 @@ async fn test_leaving_project(
|
||||||
});
|
});
|
||||||
|
|
||||||
project_b2.read_with(cx_b, |project, _| {
|
project_b2.read_with(cx_b, |project, _| {
|
||||||
assert!(project.is_read_only());
|
assert!(project.is_disconnected());
|
||||||
});
|
});
|
||||||
|
|
||||||
project_c.read_with(cx_c, |project, _| {
|
project_c.read_with(cx_c, |project, _| {
|
||||||
|
@ -3550,6 +3551,7 @@ async fn test_leaving_project(
|
||||||
client_b.user_store().clone(),
|
client_b.user_store().clone(),
|
||||||
client_b.language_registry().clone(),
|
client_b.language_registry().clone(),
|
||||||
FakeFs::new(cx.background_executor().clone()),
|
FakeFs::new(cx.background_executor().clone()),
|
||||||
|
ChannelRole::Member,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -3568,11 +3570,11 @@ async fn test_leaving_project(
|
||||||
});
|
});
|
||||||
|
|
||||||
project_b2.read_with(cx_b, |project, _| {
|
project_b2.read_with(cx_b, |project, _| {
|
||||||
assert!(project.is_read_only());
|
assert!(project.is_disconnected());
|
||||||
});
|
});
|
||||||
|
|
||||||
project_c.read_with(cx_c, |project, _| {
|
project_c.read_with(cx_c, |project, _| {
|
||||||
assert!(project.is_read_only());
|
assert!(project.is_disconnected());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1149,7 +1149,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||||
Some((project, cx))
|
Some((project, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
if !guest_project.is_read_only() {
|
if !guest_project.is_disconnected() {
|
||||||
if let Some((host_project, host_cx)) = host_project {
|
if let Some((host_project, host_cx)) = host_project {
|
||||||
let host_worktree_snapshots =
|
let host_worktree_snapshots =
|
||||||
host_project.read_with(host_cx, |host_project, cx| {
|
host_project.read_with(host_cx, |host_project, cx| {
|
||||||
|
@ -1236,7 +1236,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||||
let buffers = client.buffers().clone();
|
let buffers = client.buffers().clone();
|
||||||
for (guest_project, guest_buffers) in &buffers {
|
for (guest_project, guest_buffers) in &buffers {
|
||||||
let project_id = if guest_project.read_with(client_cx, |project, _| {
|
let project_id = if guest_project.read_with(client_cx, |project, _| {
|
||||||
project.is_local() || project.is_read_only()
|
project.is_local() || project.is_disconnected()
|
||||||
}) {
|
}) {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -518,7 +518,7 @@ impl<T: RandomizedTest> TestPlan<T> {
|
||||||
for project in client.remote_projects().iter() {
|
for project in client.remote_projects().iter() {
|
||||||
project.read_with(&client_cx, |project, _| {
|
project.read_with(&client_cx, |project, _| {
|
||||||
assert!(
|
assert!(
|
||||||
project.is_read_only(),
|
project.is_disconnected(),
|
||||||
"project {:?} should be read only",
|
"project {:?} should be read only",
|
||||||
project.remote_id()
|
project.remote_id()
|
||||||
)
|
)
|
||||||
|
|
|
@ -138,12 +138,6 @@ impl ChannelView {
|
||||||
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||||
channel_buffer.clone(),
|
channel_buffer.clone(),
|
||||||
)));
|
)));
|
||||||
editor.set_read_only(
|
|
||||||
!channel_buffer
|
|
||||||
.read(cx)
|
|
||||||
.channel(cx)
|
|
||||||
.is_some_and(|c| c.can_edit_notes()),
|
|
||||||
);
|
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
let _editor_event_subscription =
|
let _editor_event_subscription =
|
||||||
|
@ -178,8 +172,7 @@ impl ChannelView {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}),
|
}),
|
||||||
ChannelBufferEvent::ChannelChanged => {
|
ChannelBufferEvent::ChannelChanged => {
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |_, cx| {
|
||||||
editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
|
|
||||||
cx.emit(editor::EditorEvent::TitleChanged);
|
cx.emit(editor::EditorEvent::TitleChanged);
|
||||||
cx.notify()
|
cx.notify()
|
||||||
});
|
});
|
||||||
|
@ -254,11 +247,11 @@ impl Item for ChannelView {
|
||||||
fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
|
fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
|
||||||
let label = if let Some(channel) = self.channel(cx) {
|
let label = if let Some(channel) = self.channel(cx) {
|
||||||
match (
|
match (
|
||||||
channel.can_edit_notes(),
|
self.channel_buffer.read(cx).buffer().read(cx).read_only(),
|
||||||
self.channel_buffer.read(cx).is_connected(),
|
self.channel_buffer.read(cx).is_connected(),
|
||||||
) {
|
) {
|
||||||
(true, true) => format!("#{}", channel.name),
|
(false, true) => format!("#{}", channel.name),
|
||||||
(false, true) => format!("#{} (read-only)", channel.name),
|
(true, true) => format!("#{} (read-only)", channel.name),
|
||||||
(_, false) => format!("#{} (disconnected)", channel.name),
|
(_, false) => format!("#{} (disconnected)", channel.name),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -151,6 +151,10 @@ 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>),
|
||||||
|
@ -380,10 +384,14 @@ 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.
|
||||||
|
@ -402,7 +410,7 @@ impl CollabPanel {
|
||||||
&Default::default(),
|
&Default::default(),
|
||||||
executor.clone(),
|
executor.clone(),
|
||||||
));
|
));
|
||||||
if !matches.is_empty() {
|
if !matches.is_empty() && !room.read_only() {
|
||||||
let user_id = user.id;
|
let user_id = user.id;
|
||||||
self.entries.push(ListEntry::CallParticipant {
|
self.entries.push(ListEntry::CallParticipant {
|
||||||
user,
|
user,
|
||||||
|
@ -430,13 +438,23 @@ impl CollabPanel {
|
||||||
// Populate remote participants.
|
// Populate remote participants.
|
||||||
self.match_candidates.clear();
|
self.match_candidates.clear();
|
||||||
self.match_candidates
|
self.match_candidates
|
||||||
.extend(room.remote_participants().iter().map(|(_, participant)| {
|
.extend(
|
||||||
StringMatchCandidate {
|
room.remote_participants()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(_, participant)| {
|
||||||
|
if participant.role == proto::ChannelRole::Guest {
|
||||||
|
guest_count += 1;
|
||||||
|
return None;
|
||||||
|
} else {
|
||||||
|
non_guest_count += 1;
|
||||||
|
}
|
||||||
|
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(),
|
||||||
char_bag: participant.user.github_login.chars().collect(),
|
char_bag: participant.user.github_login.chars().collect(),
|
||||||
}
|
})
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
let matches = executor.block(match_strings(
|
let matches = executor.block(match_strings(
|
||||||
&self.match_candidates,
|
&self.match_candidates,
|
||||||
&query,
|
&query,
|
||||||
|
@ -470,6 +488,15 @@ 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();
|
||||||
|
@ -959,6 +986,41 @@ 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, 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 {
|
||||||
|
@ -1180,6 +1242,18 @@ 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)
|
||||||
|
@ -1735,6 +1809,12 @@ 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(),
|
||||||
|
@ -1766,7 +1846,7 @@ impl CollabPanel {
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let settings = ThemeSettings::get_global(cx);
|
let settings = ThemeSettings::get_global(cx);
|
||||||
let text_style = TextStyle {
|
let text_style = TextStyle {
|
||||||
color: if editor.read(cx).read_only() {
|
color: if editor.read(cx).read_only(cx) {
|
||||||
cx.theme().colors().text_disabled
|
cx.theme().colors().text_disabled
|
||||||
} else {
|
} else {
|
||||||
cx.theme().colors().text
|
cx.theme().colors().text
|
||||||
|
@ -2538,6 +2618,11 @@ impl PartialEq for ListEntry {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ListEntry::GuestCount { .. } => {
|
||||||
|
if let ListEntry::GuestCount { .. } = other {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use project::{Project, RepositoryEntry};
|
use project::{Project, RepositoryEntry};
|
||||||
use recent_projects::RecentProjects;
|
use recent_projects::RecentProjects;
|
||||||
|
use rpc::proto;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use theme::{ActiveTheme, PlayerColors};
|
use theme::{ActiveTheme, PlayerColors};
|
||||||
use ui::{
|
use ui::{
|
||||||
|
@ -175,8 +176,9 @@ impl Render for CollabTitlebarItem {
|
||||||
let is_muted = room.is_muted(cx);
|
let is_muted = room.is_muted(cx);
|
||||||
let is_deafened = room.is_deafened().unwrap_or(false);
|
let is_deafened = room.is_deafened().unwrap_or(false);
|
||||||
let is_screen_sharing = room.is_screen_sharing();
|
let is_screen_sharing = room.is_screen_sharing();
|
||||||
|
let read_only = room.read_only();
|
||||||
|
|
||||||
this.when(is_local, |this| {
|
this.when(is_local && !read_only, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Button::new(
|
Button::new(
|
||||||
"toggle_sharing",
|
"toggle_sharing",
|
||||||
|
@ -207,7 +209,8 @@ impl Render for CollabTitlebarItem {
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.when(!read_only, |this| {
|
||||||
|
this.child(
|
||||||
IconButton::new(
|
IconButton::new(
|
||||||
"mute-microphone",
|
"mute-microphone",
|
||||||
if is_muted {
|
if is_muted {
|
||||||
|
@ -217,11 +220,12 @@ impl Render for CollabTitlebarItem {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.style(ButtonStyle::Subtle)
|
.style(ButtonStyle::Subtle)
|
||||||
.selected_style(ButtonStyle::Tinted(TintColor::Negative))
|
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.selected(is_muted)
|
.selected(is_muted)
|
||||||
|
.selected_style(ButtonStyle::Tinted(TintColor::Negative))
|
||||||
.on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
|
.on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
|
||||||
)
|
)
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
IconButton::new(
|
IconButton::new(
|
||||||
"mute-sound",
|
"mute-sound",
|
||||||
|
@ -236,21 +240,32 @@ impl Render for CollabTitlebarItem {
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.selected(is_deafened)
|
.selected(is_deafened)
|
||||||
.tooltip(move |cx| {
|
.tooltip(move |cx| {
|
||||||
Tooltip::with_meta("Deafen Audio", None, "Mic will be muted", cx)
|
if !read_only {
|
||||||
})
|
Tooltip::with_meta(
|
||||||
.on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
|
"Deafen Audio",
|
||||||
|
None,
|
||||||
|
"Mic will be muted",
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.child(
|
} else {
|
||||||
|
Tooltip::text("Deafen Audio", cx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)),
|
||||||
|
)
|
||||||
|
.when(!read_only, |this| {
|
||||||
|
this.child(
|
||||||
IconButton::new("screen-share", ui::Icon::Screen)
|
IconButton::new("screen-share", ui::Icon::Screen)
|
||||||
.style(ButtonStyle::Subtle)
|
.style(ButtonStyle::Subtle)
|
||||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.selected(is_screen_sharing)
|
.selected(is_screen_sharing)
|
||||||
|
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||||
.on_click(move |_, cx| {
|
.on_click(move |_, cx| {
|
||||||
crate::toggle_screen_sharing(&Default::default(), cx)
|
crate::toggle_screen_sharing(&Default::default(), cx)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
})
|
||||||
.map(|el| {
|
.map(|el| {
|
||||||
let status = self.client.status();
|
let status = self.client.status();
|
||||||
let status = &*status.borrow();
|
let status = &*status.borrow();
|
||||||
|
@ -414,6 +429,10 @@ impl CollabTitlebarItem {
|
||||||
current_user: &Arc<User>,
|
current_user: &Arc<User>,
|
||||||
cx: &ViewContext<Self>,
|
cx: &ViewContext<Self>,
|
||||||
) -> Option<FacePile> {
|
) -> Option<FacePile> {
|
||||||
|
if room.role_for_user(user.id) == Some(proto::ChannelRole::Guest) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
|
let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
|
||||||
|
|
||||||
let pile = FacePile::default()
|
let pile = FacePile::default()
|
||||||
|
|
|
@ -151,7 +151,12 @@ impl ProjectDiagnosticsEditor {
|
||||||
let focus_in_subscription =
|
let focus_in_subscription =
|
||||||
cx.on_focus_in(&focus_handle, |diagnostics, cx| diagnostics.focus_in(cx));
|
cx.on_focus_in(&focus_handle, |diagnostics, cx| diagnostics.focus_in(cx));
|
||||||
|
|
||||||
let excerpts = cx.new_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
|
let excerpts = cx.new_model(|cx| {
|
||||||
|
MultiBuffer::new(
|
||||||
|
project_handle.read(cx).replica_id(),
|
||||||
|
project_handle.read(cx).capability(),
|
||||||
|
)
|
||||||
|
});
|
||||||
let editor = cx.new_view(|cx| {
|
let editor = cx.new_view(|cx| {
|
||||||
let mut editor =
|
let mut editor =
|
||||||
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
|
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
|
||||||
|
|
|
@ -54,10 +54,10 @@ use itertools::Itertools;
|
||||||
pub use language::{char_kind, CharKind};
|
pub use language::{char_kind, CharKind};
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{self, all_language_settings, InlayHintSettings},
|
language_settings::{self, all_language_settings, InlayHintSettings},
|
||||||
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel,
|
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
|
||||||
Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language,
|
CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize,
|
||||||
LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal,
|
Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection,
|
||||||
TransactionId,
|
SelectionGoal, TransactionId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
|
use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
|
||||||
|
@ -2049,8 +2049,8 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_only(&self) -> bool {
|
pub fn read_only(&self, cx: &AppContext) -> bool {
|
||||||
self.read_only
|
self.read_only || self.buffer.read(cx).read_only()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_read_only(&mut self, read_only: bool) {
|
pub fn set_read_only(&mut self, read_only: bool) {
|
||||||
|
@ -2199,7 +2199,7 @@ impl Editor {
|
||||||
S: ToOffset,
|
S: ToOffset,
|
||||||
T: Into<Arc<str>>,
|
T: Into<Arc<str>>,
|
||||||
{
|
{
|
||||||
if self.read_only {
|
if self.read_only(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2213,7 +2213,7 @@ impl Editor {
|
||||||
S: ToOffset,
|
S: ToOffset,
|
||||||
T: Into<Arc<str>>,
|
T: Into<Arc<str>>,
|
||||||
{
|
{
|
||||||
if self.read_only {
|
if self.read_only(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2232,7 +2232,7 @@ impl Editor {
|
||||||
S: ToOffset,
|
S: ToOffset,
|
||||||
T: Into<Arc<str>>,
|
T: Into<Arc<str>>,
|
||||||
{
|
{
|
||||||
if self.read_only {
|
if self.read_only(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2596,7 +2596,7 @@ impl Editor {
|
||||||
pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
||||||
let text: Arc<str> = text.into();
|
let text: Arc<str> = text.into();
|
||||||
|
|
||||||
if self.read_only {
|
if self.read_only(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3049,7 +3049,7 @@ impl Editor {
|
||||||
autoindent_mode: Option<AutoindentMode>,
|
autoindent_mode: Option<AutoindentMode>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
if self.read_only {
|
if self.read_only(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3786,7 +3786,8 @@ impl Editor {
|
||||||
|
|
||||||
let mut ranges_to_highlight = Vec::new();
|
let mut ranges_to_highlight = Vec::new();
|
||||||
let excerpt_buffer = cx.new_model(|cx| {
|
let excerpt_buffer = cx.new_model(|cx| {
|
||||||
let mut multibuffer = MultiBuffer::new(replica_id).with_title(title);
|
let mut multibuffer =
|
||||||
|
MultiBuffer::new(replica_id, Capability::ReadWrite).with_title(title);
|
||||||
for (buffer_handle, transaction) in &entries {
|
for (buffer_handle, transaction) in &entries {
|
||||||
let buffer = buffer_handle.read(cx);
|
let buffer = buffer_handle.read(cx);
|
||||||
ranges_to_highlight.extend(
|
ranges_to_highlight.extend(
|
||||||
|
@ -7491,9 +7492,10 @@ impl Editor {
|
||||||
locations.sort_by_key(|location| location.buffer.read(cx).remote_id());
|
locations.sort_by_key(|location| location.buffer.read(cx).remote_id());
|
||||||
let mut locations = locations.into_iter().peekable();
|
let mut locations = locations.into_iter().peekable();
|
||||||
let mut ranges_to_highlight = Vec::new();
|
let mut ranges_to_highlight = Vec::new();
|
||||||
|
let capability = workspace.project().read(cx).capability();
|
||||||
|
|
||||||
let excerpt_buffer = cx.new_model(|cx| {
|
let excerpt_buffer = cx.new_model(|cx| {
|
||||||
let mut multibuffer = MultiBuffer::new(replica_id);
|
let mut multibuffer = MultiBuffer::new(replica_id, capability);
|
||||||
while let Some(location) = locations.next() {
|
while let Some(location) = locations.next() {
|
||||||
let buffer = location.buffer.read(cx);
|
let buffer = location.buffer.read(cx);
|
||||||
let mut ranges_for_buffer = Vec::new();
|
let mut ranges_for_buffer = Vec::new();
|
||||||
|
@ -8608,7 +8610,8 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_local_cursors(&self, cx: &WindowContext) -> bool {
|
pub fn show_local_cursors(&self, cx: &WindowContext) -> bool {
|
||||||
self.blink_manager.read(cx).visible() && self.focus_handle.is_focused(cx)
|
(self.read_only(cx) || self.blink_manager.read(cx).visible())
|
||||||
|
&& self.focus_handle.is_focused(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_buffer_changed(&mut self, _: Model<MultiBuffer>, cx: &mut ViewContext<Self>) {
|
fn on_buffer_changed(&mut self, _: Model<MultiBuffer>, cx: &mut ViewContext<Self>) {
|
||||||
|
|
|
@ -17,8 +17,9 @@ use gpui::{
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
||||||
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
|
BracketPairConfig,
|
||||||
Override, Point,
|
Capability::ReadWrite,
|
||||||
|
FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, Override, Point,
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::project_settings::{LspSettings, ProjectSettings};
|
use project::project_settings::{LspSettings, ProjectSettings};
|
||||||
|
@ -2355,7 +2356,7 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
|
||||||
.with_language(rust_language, cx)
|
.with_language(rust_language, cx)
|
||||||
});
|
});
|
||||||
let multibuffer = cx.new_model(|cx| {
|
let multibuffer = cx.new_model(|cx| {
|
||||||
let mut multibuffer = MultiBuffer::new(0);
|
let mut multibuffer = MultiBuffer::new(0, ReadWrite);
|
||||||
multibuffer.push_excerpts(
|
multibuffer.push_excerpts(
|
||||||
toml_buffer.clone(),
|
toml_buffer.clone(),
|
||||||
[ExcerptRange {
|
[ExcerptRange {
|
||||||
|
@ -6019,7 +6020,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
|
let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
|
||||||
let multibuffer = cx.new_model(|cx| {
|
let multibuffer = cx.new_model(|cx| {
|
||||||
let mut multibuffer = MultiBuffer::new(0);
|
let mut multibuffer = MultiBuffer::new(0, ReadWrite);
|
||||||
multibuffer.push_excerpts(
|
multibuffer.push_excerpts(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
[
|
[
|
||||||
|
@ -6103,7 +6104,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
|
||||||
});
|
});
|
||||||
let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), initial_text));
|
let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), initial_text));
|
||||||
let multibuffer = cx.new_model(|cx| {
|
let multibuffer = cx.new_model(|cx| {
|
||||||
let mut multibuffer = MultiBuffer::new(0);
|
let mut multibuffer = MultiBuffer::new(0, ReadWrite);
|
||||||
multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
|
multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
|
||||||
multibuffer
|
multibuffer
|
||||||
});
|
});
|
||||||
|
@ -6162,7 +6163,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
|
||||||
let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
|
let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
|
||||||
let mut excerpt1_id = None;
|
let mut excerpt1_id = None;
|
||||||
let multibuffer = cx.new_model(|cx| {
|
let multibuffer = cx.new_model(|cx| {
|
||||||
let mut multibuffer = MultiBuffer::new(0);
|
let mut multibuffer = MultiBuffer::new(0, ReadWrite);
|
||||||
excerpt1_id = multibuffer
|
excerpt1_id = multibuffer
|
||||||
.push_excerpts(
|
.push_excerpts(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
|
@ -6247,7 +6248,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
|
||||||
let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
|
let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
|
||||||
let mut excerpt1_id = None;
|
let mut excerpt1_id = None;
|
||||||
let multibuffer = cx.new_model(|cx| {
|
let multibuffer = cx.new_model(|cx| {
|
||||||
let mut multibuffer = MultiBuffer::new(0);
|
let mut multibuffer = MultiBuffer::new(0, ReadWrite);
|
||||||
excerpt1_id = multibuffer
|
excerpt1_id = multibuffer
|
||||||
.push_excerpts(
|
.push_excerpts(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
|
@ -6636,7 +6637,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||||
|
|
||||||
let leader = pane.update(cx, |_, cx| {
|
let leader = pane.update(cx, |_, cx| {
|
||||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
|
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, ReadWrite));
|
||||||
cx.new_view(|cx| build_editor(multibuffer.clone(), cx))
|
cx.new_view(|cx| build_editor(multibuffer.clone(), cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7425,7 +7426,7 @@ async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::T
|
||||||
let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n"));
|
let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n"));
|
||||||
let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "c = 3\nd = 4\n"));
|
let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "c = 3\nd = 4\n"));
|
||||||
let multibuffer = cx.new_model(|cx| {
|
let multibuffer = cx.new_model(|cx| {
|
||||||
let mut multibuffer = MultiBuffer::new(0);
|
let mut multibuffer = MultiBuffer::new(0, ReadWrite);
|
||||||
multibuffer.push_excerpts(
|
multibuffer.push_excerpts(
|
||||||
buffer_1.clone(),
|
buffer_1.clone(),
|
||||||
[ExcerptRange {
|
[ExcerptRange {
|
||||||
|
@ -7552,7 +7553,7 @@ async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let multibuffer = cx.new_model(|cx| {
|
let multibuffer = cx.new_model(|cx| {
|
||||||
let mut multibuffer = MultiBuffer::new(0);
|
let mut multibuffer = MultiBuffer::new(0, ReadWrite);
|
||||||
multibuffer.push_excerpts(
|
multibuffer.push_excerpts(
|
||||||
private_buffer.clone(),
|
private_buffer.clone(),
|
||||||
[ExcerptRange {
|
[ExcerptRange {
|
||||||
|
|
|
@ -1910,7 +1910,13 @@ impl EditorElement {
|
||||||
layouts.push(layout);
|
layouts.push(layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
selections.push((style.local_player, layouts));
|
let player = if editor.read_only(cx) {
|
||||||
|
cx.theme().players().read_only()
|
||||||
|
} else {
|
||||||
|
style.local_player
|
||||||
|
};
|
||||||
|
|
||||||
|
selections.push((player, layouts));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(collaboration_hub) = &editor.collaboration_hub {
|
if let Some(collaboration_hub) = &editor.collaboration_hub {
|
||||||
|
|
|
@ -93,6 +93,7 @@ mod tests {
|
||||||
use crate::editor_tests::init_test;
|
use crate::editor_tests::init_test;
|
||||||
use crate::Point;
|
use crate::Point;
|
||||||
use gpui::{Context, TestAppContext};
|
use gpui::{Context, TestAppContext};
|
||||||
|
use language::Capability::ReadWrite;
|
||||||
use multi_buffer::{ExcerptRange, MultiBuffer};
|
use multi_buffer::{ExcerptRange, MultiBuffer};
|
||||||
use project::{FakeFs, Project};
|
use project::{FakeFs, Project};
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
|
@ -183,7 +184,7 @@ mod tests {
|
||||||
cx.background_executor.run_until_parked();
|
cx.background_executor.run_until_parked();
|
||||||
|
|
||||||
let multibuffer = cx.new_model(|cx| {
|
let multibuffer = cx.new_model(|cx| {
|
||||||
let mut multibuffer = MultiBuffer::new(0);
|
let mut multibuffer = MultiBuffer::new(0, ReadWrite);
|
||||||
multibuffer.push_excerpts(
|
multibuffer.push_excerpts(
|
||||||
buffer_1.clone(),
|
buffer_1.clone(),
|
||||||
[
|
[
|
||||||
|
|
|
@ -1206,7 +1206,8 @@ pub mod tests {
|
||||||
use gpui::{Context, TestAppContext, WindowHandle};
|
use gpui::{Context, TestAppContext, WindowHandle};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
|
language_settings::AllLanguageSettingsContent, Capability, FakeLspAdapter, Language,
|
||||||
|
LanguageConfig,
|
||||||
};
|
};
|
||||||
use lsp::FakeLanguageServer;
|
use lsp::FakeLanguageServer;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -2459,7 +2460,7 @@ pub mod tests {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let multibuffer = cx.new_model(|cx| {
|
let multibuffer = cx.new_model(|cx| {
|
||||||
let mut multibuffer = MultiBuffer::new(0);
|
let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite);
|
||||||
multibuffer.push_excerpts(
|
multibuffer.push_excerpts(
|
||||||
buffer_1.clone(),
|
buffer_1.clone(),
|
||||||
[
|
[
|
||||||
|
@ -2798,7 +2799,7 @@ pub mod tests {
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
|
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||||
let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
|
let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
|
||||||
let buffer_1_excerpts = multibuffer.push_excerpts(
|
let buffer_1_excerpts = multibuffer.push_excerpts(
|
||||||
buffer_1.clone(),
|
buffer_1.clone(),
|
||||||
|
|
|
@ -103,7 +103,8 @@ impl FollowableItem for Editor {
|
||||||
if state.singleton && buffers.len() == 1 {
|
if state.singleton && buffers.len() == 1 {
|
||||||
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
|
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
|
||||||
} else {
|
} else {
|
||||||
multibuffer = MultiBuffer::new(replica_id);
|
multibuffer =
|
||||||
|
MultiBuffer::new(replica_id, project.read(cx).capability());
|
||||||
let mut excerpts = state.excerpts.into_iter().peekable();
|
let mut excerpts = state.excerpts.into_iter().peekable();
|
||||||
while let Some(excerpt) = excerpts.peek() {
|
while let Some(excerpt) = excerpts.peek() {
|
||||||
let buffer_id = excerpt.buffer_id;
|
let buffer_id = excerpt.buffer_id;
|
||||||
|
|
|
@ -461,6 +461,7 @@ mod tests {
|
||||||
Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
|
Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
|
||||||
};
|
};
|
||||||
use gpui::{font, Context as _};
|
use gpui::{font, Context as _};
|
||||||
|
use language::Capability;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use util::post_inc;
|
use util::post_inc;
|
||||||
|
@ -766,7 +767,7 @@ mod tests {
|
||||||
let buffer =
|
let buffer =
|
||||||
cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abc\ndefg\nhijkl\nmn"));
|
cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abc\ndefg\nhijkl\nmn"));
|
||||||
let multibuffer = cx.new_model(|cx| {
|
let multibuffer = cx.new_model(|cx| {
|
||||||
let mut multibuffer = MultiBuffer::new(0);
|
let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite);
|
||||||
multibuffer.push_excerpts(
|
multibuffer.push_excerpts(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
[
|
[
|
||||||
|
|
|
@ -339,6 +339,15 @@ impl Hsla {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn grayscale(&self) -> Self {
|
||||||
|
Hsla {
|
||||||
|
h: self.h,
|
||||||
|
s: 0.,
|
||||||
|
l: self.l,
|
||||||
|
a: self.a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Fade out the color by a given factor. This factor should be between 0.0 and 1.0.
|
/// Fade out the color by a given factor. This factor should be between 0.0 and 1.0.
|
||||||
/// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color.
|
/// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color.
|
||||||
pub fn fade_out(&mut self, factor: f32) {
|
pub fn fade_out(&mut self, factor: f32) {
|
||||||
|
|
|
@ -44,8 +44,9 @@ pub trait IntoElement: Sized {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert into an element, then draw in the current window at the given origin.
|
/// Convert into an element, then draw in the current window at the given origin.
|
||||||
/// The provided available space is provided to the layout engine to determine the size of the root element.
|
/// The available space argument is provided to the layout engine to determine the size of the
|
||||||
/// Once the element is drawn, its associated element staet is yielded to the given callback.
|
// root element. Once the element is drawn, its associated element state is yielded to the
|
||||||
|
// given callback.
|
||||||
fn draw_and_update_state<T, R>(
|
fn draw_and_update_state<T, R>(
|
||||||
self,
|
self,
|
||||||
origin: Point<Pixels>,
|
origin: Point<Pixels>,
|
||||||
|
|
|
@ -57,6 +57,12 @@ lazy_static! {
|
||||||
pub static ref BUFFER_DIFF_TASK: TaskLabel = TaskLabel::new();
|
pub static ref BUFFER_DIFF_TASK: TaskLabel = TaskLabel::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Copy, Debug)]
|
||||||
|
pub enum Capability {
|
||||||
|
ReadWrite,
|
||||||
|
ReadOnly,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Buffer {
|
pub struct Buffer {
|
||||||
text: TextBuffer,
|
text: TextBuffer,
|
||||||
diff_base: Option<String>,
|
diff_base: Option<String>,
|
||||||
|
@ -90,6 +96,7 @@ pub struct Buffer {
|
||||||
completion_triggers: Vec<String>,
|
completion_triggers: Vec<String>,
|
||||||
completion_triggers_timestamp: clock::Lamport,
|
completion_triggers_timestamp: clock::Lamport,
|
||||||
deferred_ops: OperationQueue<Operation>,
|
deferred_ops: OperationQueue<Operation>,
|
||||||
|
capability: Capability,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BufferSnapshot {
|
pub struct BufferSnapshot {
|
||||||
|
@ -405,19 +412,27 @@ impl Buffer {
|
||||||
TextBuffer::new(replica_id, id, base_text.into()),
|
TextBuffer::new(replica_id, id, base_text.into()),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
Capability::ReadWrite,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remote(remote_id: u64, replica_id: ReplicaId, base_text: String) -> Self {
|
pub fn remote(
|
||||||
|
remote_id: u64,
|
||||||
|
replica_id: ReplicaId,
|
||||||
|
capability: Capability,
|
||||||
|
base_text: String,
|
||||||
|
) -> Self {
|
||||||
Self::build(
|
Self::build(
|
||||||
TextBuffer::new(replica_id, remote_id, base_text),
|
TextBuffer::new(replica_id, remote_id, base_text),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
capability,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_proto(
|
pub fn from_proto(
|
||||||
replica_id: ReplicaId,
|
replica_id: ReplicaId,
|
||||||
|
capability: Capability,
|
||||||
message: proto::BufferState,
|
message: proto::BufferState,
|
||||||
file: Option<Arc<dyn File>>,
|
file: Option<Arc<dyn File>>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
|
@ -426,6 +441,7 @@ impl Buffer {
|
||||||
buffer,
|
buffer,
|
||||||
message.diff_base.map(|text| text.into_boxed_str().into()),
|
message.diff_base.map(|text| text.into_boxed_str().into()),
|
||||||
file,
|
file,
|
||||||
|
capability,
|
||||||
);
|
);
|
||||||
this.text.set_line_ending(proto::deserialize_line_ending(
|
this.text.set_line_ending(proto::deserialize_line_ending(
|
||||||
rpc::proto::LineEnding::from_i32(message.line_ending)
|
rpc::proto::LineEnding::from_i32(message.line_ending)
|
||||||
|
@ -504,10 +520,19 @@ impl Buffer {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn capability(&self) -> Capability {
|
||||||
|
self.capability
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_only(&self) -> bool {
|
||||||
|
self.capability == Capability::ReadOnly
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build(
|
pub fn build(
|
||||||
buffer: TextBuffer,
|
buffer: TextBuffer,
|
||||||
diff_base: Option<String>,
|
diff_base: Option<String>,
|
||||||
file: Option<Arc<dyn File>>,
|
file: Option<Arc<dyn File>>,
|
||||||
|
capability: Capability,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let saved_mtime = if let Some(file) = file.as_ref() {
|
let saved_mtime = if let Some(file) = file.as_ref() {
|
||||||
file.mtime()
|
file.mtime()
|
||||||
|
@ -526,6 +551,7 @@ impl Buffer {
|
||||||
diff_base,
|
diff_base,
|
||||||
git_diff: git::diff::BufferDiff::new(),
|
git_diff: git::diff::BufferDiff::new(),
|
||||||
file,
|
file,
|
||||||
|
capability,
|
||||||
syntax_map: Mutex::new(SyntaxMap::new()),
|
syntax_map: Mutex::new(SyntaxMap::new()),
|
||||||
parsing_in_background: false,
|
parsing_in_background: false,
|
||||||
parse_count: 0,
|
parse_count: 0,
|
||||||
|
|
|
@ -1926,7 +1926,7 @@ fn test_serialization(cx: &mut gpui::AppContext) {
|
||||||
.background_executor()
|
.background_executor()
|
||||||
.block(buffer1.read(cx).serialize_ops(None, cx));
|
.block(buffer1.read(cx).serialize_ops(None, cx));
|
||||||
let buffer2 = cx.new_model(|cx| {
|
let buffer2 = cx.new_model(|cx| {
|
||||||
let mut buffer = Buffer::from_proto(1, state, None).unwrap();
|
let mut buffer = Buffer::from_proto(1, Capability::ReadWrite, state, None).unwrap();
|
||||||
buffer
|
buffer
|
||||||
.apply_ops(
|
.apply_ops(
|
||||||
ops.into_iter()
|
ops.into_iter()
|
||||||
|
@ -1967,7 +1967,8 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
|
||||||
let ops = cx
|
let ops = cx
|
||||||
.background_executor()
|
.background_executor()
|
||||||
.block(base_buffer.read(cx).serialize_ops(None, cx));
|
.block(base_buffer.read(cx).serialize_ops(None, cx));
|
||||||
let mut buffer = Buffer::from_proto(i as ReplicaId, state, None).unwrap();
|
let mut buffer =
|
||||||
|
Buffer::from_proto(i as ReplicaId, Capability::ReadWrite, state, None).unwrap();
|
||||||
buffer
|
buffer
|
||||||
.apply_ops(
|
.apply_ops(
|
||||||
ops.into_iter()
|
ops.into_iter()
|
||||||
|
@ -2083,8 +2084,13 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
|
||||||
replica_id
|
replica_id
|
||||||
);
|
);
|
||||||
new_buffer = Some(cx.new_model(|cx| {
|
new_buffer = Some(cx.new_model(|cx| {
|
||||||
let mut new_buffer =
|
let mut new_buffer = Buffer::from_proto(
|
||||||
Buffer::from_proto(new_replica_id, old_buffer_state, None).unwrap();
|
new_replica_id,
|
||||||
|
Capability::ReadWrite,
|
||||||
|
old_buffer_state,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
new_buffer
|
new_buffer
|
||||||
.apply_ops(
|
.apply_ops(
|
||||||
old_buffer_ops
|
old_buffer_ops
|
||||||
|
|
|
@ -62,6 +62,7 @@ impl<'a> VideoGrant<'a> {
|
||||||
Self {
|
Self {
|
||||||
room: Some(Cow::Borrowed(room)),
|
room: Some(Cow::Borrowed(room)),
|
||||||
room_join: Some(true),
|
room_join: Some(true),
|
||||||
|
can_publish: Some(false),
|
||||||
can_subscribe: Some(true),
|
can_subscribe: Some(true),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ pub use language::Completion;
|
||||||
use language::{
|
use language::{
|
||||||
char_kind,
|
char_kind,
|
||||||
language_settings::{language_settings, LanguageSettings},
|
language_settings::{language_settings, LanguageSettings},
|
||||||
AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
|
AutoindentMode, Buffer, BufferChunks, BufferSnapshot, Capability, CharKind, Chunk, CursorShape,
|
||||||
DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
|
DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
|
||||||
Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
|
Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
|
||||||
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
|
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
|
||||||
|
@ -55,6 +55,7 @@ pub struct MultiBuffer {
|
||||||
replica_id: ReplicaId,
|
replica_id: ReplicaId,
|
||||||
history: History,
|
history: History,
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
|
capability: Capability,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
@ -225,13 +226,14 @@ struct ExcerptBytes<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MultiBuffer {
|
impl MultiBuffer {
|
||||||
pub fn new(replica_id: ReplicaId) -> Self {
|
pub fn new(replica_id: ReplicaId, capability: Capability) -> Self {
|
||||||
Self {
|
Self {
|
||||||
snapshot: Default::default(),
|
snapshot: Default::default(),
|
||||||
buffers: Default::default(),
|
buffers: Default::default(),
|
||||||
next_excerpt_id: 1,
|
next_excerpt_id: 1,
|
||||||
subscriptions: Default::default(),
|
subscriptions: Default::default(),
|
||||||
singleton: false,
|
singleton: false,
|
||||||
|
capability,
|
||||||
replica_id,
|
replica_id,
|
||||||
history: History {
|
history: History {
|
||||||
next_transaction_id: Default::default(),
|
next_transaction_id: Default::default(),
|
||||||
|
@ -271,6 +273,7 @@ impl MultiBuffer {
|
||||||
next_excerpt_id: 1,
|
next_excerpt_id: 1,
|
||||||
subscriptions: Default::default(),
|
subscriptions: Default::default(),
|
||||||
singleton: self.singleton,
|
singleton: self.singleton,
|
||||||
|
capability: self.capability,
|
||||||
replica_id: self.replica_id,
|
replica_id: self.replica_id,
|
||||||
history: self.history.clone(),
|
history: self.history.clone(),
|
||||||
title: self.title.clone(),
|
title: self.title.clone(),
|
||||||
|
@ -282,8 +285,12 @@ impl MultiBuffer {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_only(&self) -> bool {
|
||||||
|
self.capability == Capability::ReadOnly
|
||||||
|
}
|
||||||
|
|
||||||
pub fn singleton(buffer: Model<Buffer>, cx: &mut ModelContext<Self>) -> Self {
|
pub fn singleton(buffer: Model<Buffer>, cx: &mut ModelContext<Self>) -> Self {
|
||||||
let mut this = Self::new(buffer.read(cx).replica_id());
|
let mut this = Self::new(buffer.read(cx).replica_id(), buffer.read(cx).capability());
|
||||||
this.singleton = true;
|
this.singleton = true;
|
||||||
this.push_excerpts(
|
this.push_excerpts(
|
||||||
buffer,
|
buffer,
|
||||||
|
@ -1657,7 +1664,7 @@ impl MultiBuffer {
|
||||||
excerpts: [(&str, Vec<Range<Point>>); COUNT],
|
excerpts: [(&str, Vec<Range<Point>>); COUNT],
|
||||||
cx: &mut gpui::AppContext,
|
cx: &mut gpui::AppContext,
|
||||||
) -> Model<Self> {
|
) -> Model<Self> {
|
||||||
let multi = cx.new_model(|_| Self::new(0));
|
let multi = cx.new_model(|_| Self::new(0, Capability::ReadWrite));
|
||||||
for (text, ranges) in excerpts {
|
for (text, ranges) in excerpts {
|
||||||
let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text));
|
let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text));
|
||||||
let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange {
|
let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange {
|
||||||
|
@ -1678,7 +1685,7 @@ impl MultiBuffer {
|
||||||
|
|
||||||
pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::AppContext) -> Model<Self> {
|
pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::AppContext) -> Model<Self> {
|
||||||
cx.new_model(|cx| {
|
cx.new_model(|cx| {
|
||||||
let mut multibuffer = MultiBuffer::new(0);
|
let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite);
|
||||||
let mutation_count = rng.gen_range(1..=5);
|
let mutation_count = rng.gen_range(1..=5);
|
||||||
multibuffer.randomly_edit_excerpts(rng, mutation_count, cx);
|
multibuffer.randomly_edit_excerpts(rng, mutation_count, cx);
|
||||||
multibuffer
|
multibuffer
|
||||||
|
@ -4176,7 +4183,7 @@ mod tests {
|
||||||
let ops = cx
|
let ops = cx
|
||||||
.background_executor()
|
.background_executor()
|
||||||
.block(host_buffer.read(cx).serialize_ops(None, cx));
|
.block(host_buffer.read(cx).serialize_ops(None, cx));
|
||||||
let mut buffer = Buffer::from_proto(1, state, None).unwrap();
|
let mut buffer = Buffer::from_proto(1, Capability::ReadWrite, state, None).unwrap();
|
||||||
buffer
|
buffer
|
||||||
.apply_ops(
|
.apply_ops(
|
||||||
ops.into_iter()
|
ops.into_iter()
|
||||||
|
@ -4205,7 +4212,7 @@ mod tests {
|
||||||
cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'a')));
|
cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'a')));
|
||||||
let buffer_2 =
|
let buffer_2 =
|
||||||
cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'g')));
|
cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'g')));
|
||||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
|
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||||
|
|
||||||
let events = Arc::new(RwLock::new(Vec::<Event>::new()));
|
let events = Arc::new(RwLock::new(Vec::<Event>::new()));
|
||||||
multibuffer.update(cx, |_, cx| {
|
multibuffer.update(cx, |_, cx| {
|
||||||
|
@ -4442,8 +4449,8 @@ mod tests {
|
||||||
let buffer_2 =
|
let buffer_2 =
|
||||||
cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(10, 3, 'm')));
|
cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(10, 3, 'm')));
|
||||||
|
|
||||||
let leader_multibuffer = cx.new_model(|_| MultiBuffer::new(0));
|
let leader_multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||||
let follower_multibuffer = cx.new_model(|_| MultiBuffer::new(0));
|
let follower_multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||||
let follower_edit_event_count = Arc::new(RwLock::new(0));
|
let follower_edit_event_count = Arc::new(RwLock::new(0));
|
||||||
|
|
||||||
follower_multibuffer.update(cx, |_, cx| {
|
follower_multibuffer.update(cx, |_, cx| {
|
||||||
|
@ -4547,7 +4554,7 @@ mod tests {
|
||||||
fn test_push_excerpts_with_context_lines(cx: &mut AppContext) {
|
fn test_push_excerpts_with_context_lines(cx: &mut AppContext) {
|
||||||
let buffer =
|
let buffer =
|
||||||
cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(20, 3, 'a')));
|
cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(20, 3, 'a')));
|
||||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
|
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||||
let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
|
let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
|
||||||
multibuffer.push_excerpts_with_context_lines(
|
multibuffer.push_excerpts_with_context_lines(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
|
@ -4584,7 +4591,7 @@ mod tests {
|
||||||
async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) {
|
async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) {
|
||||||
let buffer =
|
let buffer =
|
||||||
cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(20, 3, 'a')));
|
cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(20, 3, 'a')));
|
||||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
|
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||||
let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
|
let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
|
||||||
let snapshot = buffer.read(cx);
|
let snapshot = buffer.read(cx);
|
||||||
let ranges = vec![
|
let ranges = vec![
|
||||||
|
@ -4619,7 +4626,7 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_empty_multibuffer(cx: &mut AppContext) {
|
fn test_empty_multibuffer(cx: &mut AppContext) {
|
||||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
|
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||||
|
|
||||||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||||
assert_eq!(snapshot.text(), "");
|
assert_eq!(snapshot.text(), "");
|
||||||
|
@ -4652,7 +4659,7 @@ mod tests {
|
||||||
let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd"));
|
let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd"));
|
||||||
let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "efghi"));
|
let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "efghi"));
|
||||||
let multibuffer = cx.new_model(|cx| {
|
let multibuffer = cx.new_model(|cx| {
|
||||||
let mut multibuffer = MultiBuffer::new(0);
|
let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite);
|
||||||
multibuffer.push_excerpts(
|
multibuffer.push_excerpts(
|
||||||
buffer_1.clone(),
|
buffer_1.clone(),
|
||||||
[ExcerptRange {
|
[ExcerptRange {
|
||||||
|
@ -4710,7 +4717,7 @@ mod tests {
|
||||||
let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd"));
|
let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd"));
|
||||||
let buffer_2 =
|
let buffer_2 =
|
||||||
cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "ABCDEFGHIJKLMNOP"));
|
cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "ABCDEFGHIJKLMNOP"));
|
||||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
|
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||||
|
|
||||||
// Create an insertion id in buffer 1 that doesn't exist in buffer 2.
|
// Create an insertion id in buffer 1 that doesn't exist in buffer 2.
|
||||||
// Add an excerpt from buffer 1 that spans this new insertion.
|
// Add an excerpt from buffer 1 that spans this new insertion.
|
||||||
|
@ -4844,7 +4851,7 @@ mod tests {
|
||||||
.unwrap_or(10);
|
.unwrap_or(10);
|
||||||
|
|
||||||
let mut buffers: Vec<Model<Buffer>> = Vec::new();
|
let mut buffers: Vec<Model<Buffer>> = Vec::new();
|
||||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
|
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||||
let mut excerpt_ids = Vec::<ExcerptId>::new();
|
let mut excerpt_ids = Vec::<ExcerptId>::new();
|
||||||
let mut expected_excerpts = Vec::<(Model<Buffer>, Range<text::Anchor>)>::new();
|
let mut expected_excerpts = Vec::<(Model<Buffer>, Range<text::Anchor>)>::new();
|
||||||
let mut anchors = Vec::new();
|
let mut anchors = Vec::new();
|
||||||
|
@ -5266,7 +5273,7 @@ mod tests {
|
||||||
|
|
||||||
let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "1234"));
|
let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "1234"));
|
||||||
let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "5678"));
|
let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "5678"));
|
||||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
|
let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
|
||||||
let group_interval = multibuffer.read(cx).history.group_interval;
|
let group_interval = multibuffer.read(cx).history.group_interval;
|
||||||
multibuffer.update(cx, |multibuffer, cx| {
|
multibuffer.update(cx, |multibuffer, cx| {
|
||||||
multibuffer.push_excerpts(
|
multibuffer.push_excerpts(
|
||||||
|
|
|
@ -39,11 +39,11 @@ use language::{
|
||||||
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
||||||
serialize_anchor, serialize_version, split_operations,
|
serialize_anchor, serialize_version, split_operations,
|
||||||
},
|
},
|
||||||
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction,
|
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability,
|
||||||
CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
|
CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
|
||||||
File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate,
|
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
|
||||||
OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
|
LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16,
|
||||||
ToOffset, ToPointUtf16, Transaction, Unclipped,
|
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||||
};
|
};
|
||||||
use log::error;
|
use log::error;
|
||||||
use lsp::{
|
use lsp::{
|
||||||
|
@ -262,6 +262,7 @@ enum ProjectClientState {
|
||||||
},
|
},
|
||||||
Remote {
|
Remote {
|
||||||
sharing_has_stopped: bool,
|
sharing_has_stopped: bool,
|
||||||
|
capability: Capability,
|
||||||
remote_id: u64,
|
remote_id: u64,
|
||||||
replica_id: ReplicaId,
|
replica_id: ReplicaId,
|
||||||
},
|
},
|
||||||
|
@ -702,6 +703,7 @@ impl Project {
|
||||||
user_store: Model<UserStore>,
|
user_store: Model<UserStore>,
|
||||||
languages: Arc<LanguageRegistry>,
|
languages: Arc<LanguageRegistry>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
role: proto::ChannelRole,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) -> Result<Model<Self>> {
|
) -> Result<Model<Self>> {
|
||||||
client.authenticate_and_connect(true, &cx).await?;
|
client.authenticate_and_connect(true, &cx).await?;
|
||||||
|
@ -756,6 +758,7 @@ impl Project {
|
||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
client_state: Some(ProjectClientState::Remote {
|
client_state: Some(ProjectClientState::Remote {
|
||||||
sharing_has_stopped: false,
|
sharing_has_stopped: false,
|
||||||
|
capability: Capability::ReadWrite,
|
||||||
remote_id,
|
remote_id,
|
||||||
replica_id,
|
replica_id,
|
||||||
}),
|
}),
|
||||||
|
@ -796,6 +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);
|
||||||
for worktree in worktrees {
|
for worktree in worktrees {
|
||||||
let _ = this.add_worktree(&worktree, cx);
|
let _ = this.add_worktree(&worktree, cx);
|
||||||
}
|
}
|
||||||
|
@ -1618,6 +1622,17 @@ impl Project {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_role(&mut self, role: proto::ChannelRole) {
|
||||||
|
if let Some(ProjectClientState::Remote { capability, .. }) = &mut self.client_state {
|
||||||
|
*capability = if role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin
|
||||||
|
{
|
||||||
|
Capability::ReadWrite
|
||||||
|
} else {
|
||||||
|
Capability::ReadOnly
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn disconnected_from_host_internal(&mut self, cx: &mut AppContext) {
|
fn disconnected_from_host_internal(&mut self, cx: &mut AppContext) {
|
||||||
if let Some(ProjectClientState::Remote {
|
if let Some(ProjectClientState::Remote {
|
||||||
sharing_has_stopped,
|
sharing_has_stopped,
|
||||||
|
@ -1659,7 +1674,7 @@ impl Project {
|
||||||
cx.emit(Event::Closed);
|
cx.emit(Event::Closed);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_read_only(&self) -> bool {
|
pub fn is_disconnected(&self) -> bool {
|
||||||
match &self.client_state {
|
match &self.client_state {
|
||||||
Some(ProjectClientState::Remote {
|
Some(ProjectClientState::Remote {
|
||||||
sharing_has_stopped,
|
sharing_has_stopped,
|
||||||
|
@ -1669,6 +1684,17 @@ impl Project {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn capability(&self) -> Capability {
|
||||||
|
match &self.client_state {
|
||||||
|
Some(ProjectClientState::Remote { capability, .. }) => *capability,
|
||||||
|
Some(ProjectClientState::Local { .. }) | None => Capability::ReadWrite,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_read_only(&self) -> bool {
|
||||||
|
self.is_disconnected() || self.capability() == Capability::ReadOnly
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_local(&self) -> bool {
|
pub fn is_local(&self) -> bool {
|
||||||
match &self.client_state {
|
match &self.client_state {
|
||||||
Some(ProjectClientState::Remote { .. }) => false,
|
Some(ProjectClientState::Remote { .. }) => false,
|
||||||
|
@ -6013,7 +6039,7 @@ impl Project {
|
||||||
this.upgrade().context("project dropped")?;
|
this.upgrade().context("project dropped")?;
|
||||||
let response = rpc.request(message).await?;
|
let response = rpc.request(message).await?;
|
||||||
let this = this.upgrade().context("project dropped")?;
|
let this = this.upgrade().context("project dropped")?;
|
||||||
if this.update(&mut cx, |this, _| this.is_read_only())? {
|
if this.update(&mut cx, |this, _| this.is_disconnected())? {
|
||||||
Err(anyhow!("disconnected before completing request"))
|
Err(anyhow!("disconnected before completing request"))
|
||||||
} else {
|
} else {
|
||||||
request
|
request
|
||||||
|
@ -7192,7 +7218,8 @@ impl Project {
|
||||||
|
|
||||||
let buffer_id = state.id;
|
let buffer_id = state.id;
|
||||||
let buffer = cx.new_model(|_| {
|
let buffer = cx.new_model(|_| {
|
||||||
Buffer::from_proto(this.replica_id(), state, buffer_file).unwrap()
|
Buffer::from_proto(this.replica_id(), this.capability(), state, buffer_file)
|
||||||
|
.unwrap()
|
||||||
});
|
});
|
||||||
this.incomplete_remote_buffers
|
this.incomplete_remote_buffers
|
||||||
.insert(buffer_id, Some(buffer));
|
.insert(buffer_id, Some(buffer));
|
||||||
|
@ -7940,7 +7967,7 @@ impl Project {
|
||||||
|
|
||||||
if let Some(buffer) = buffer {
|
if let Some(buffer) = buffer {
|
||||||
break buffer;
|
break buffer;
|
||||||
} else if this.update(&mut cx, |this, _| this.is_read_only())? {
|
} else if this.update(&mut cx, |this, _| this.is_disconnected())? {
|
||||||
return Err(anyhow!("disconnected before buffer {} could be opened", id));
|
return Err(anyhow!("disconnected before buffer {} could be opened", id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,8 @@ use language::{
|
||||||
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
|
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
|
||||||
serialize_version,
|
serialize_version,
|
||||||
},
|
},
|
||||||
Buffer, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint, Unclipped,
|
Buffer, Capability, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint,
|
||||||
|
Unclipped,
|
||||||
};
|
};
|
||||||
use lsp::LanguageServerId;
|
use lsp::LanguageServerId;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -682,7 +683,14 @@ impl LocalWorktree {
|
||||||
.background_executor()
|
.background_executor()
|
||||||
.spawn(async move { text::Buffer::new(0, id, contents) })
|
.spawn(async move { text::Buffer::new(0, id, contents) })
|
||||||
.await;
|
.await;
|
||||||
cx.new_model(|_| Buffer::build(text_buffer, diff_base, Some(Arc::new(file))))
|
cx.new_model(|_| {
|
||||||
|
Buffer::build(
|
||||||
|
text_buffer,
|
||||||
|
diff_base,
|
||||||
|
Some(Arc::new(file)),
|
||||||
|
Capability::ReadWrite,
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -388,8 +388,18 @@ impl ProjectPanel {
|
||||||
let is_dir = entry.is_dir();
|
let is_dir = entry.is_dir();
|
||||||
let worktree_id = worktree.id();
|
let worktree_id = worktree.id();
|
||||||
let is_local = project.is_local();
|
let is_local = project.is_local();
|
||||||
|
let is_read_only = project.is_read_only();
|
||||||
|
|
||||||
let context_menu = ContextMenu::build(cx, |mut menu, cx| {
|
let context_menu = ContextMenu::build(cx, |mut menu, cx| {
|
||||||
|
if is_read_only {
|
||||||
|
menu = menu.action("Copy Relative Path", Box::new(CopyRelativePath));
|
||||||
|
if is_dir {
|
||||||
|
menu = menu.action("Search Inside", Box::new(NewSearchInDirectory))
|
||||||
|
}
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
if is_local {
|
if is_local {
|
||||||
menu = menu.action(
|
menu = menu.action(
|
||||||
"Add Folder to Project",
|
"Add Folder to Project",
|
||||||
|
@ -1473,6 +1483,7 @@ impl ProjectPanel {
|
||||||
impl Render for ProjectPanel {
|
impl Render for ProjectPanel {
|
||||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||||
let has_worktree = self.visible_entries.len() != 0;
|
let has_worktree = self.visible_entries.len() != 0;
|
||||||
|
let project = self.project.read(cx);
|
||||||
|
|
||||||
if has_worktree {
|
if has_worktree {
|
||||||
div()
|
div()
|
||||||
|
@ -1485,21 +1496,25 @@ impl Render for ProjectPanel {
|
||||||
.on_action(cx.listener(Self::expand_selected_entry))
|
.on_action(cx.listener(Self::expand_selected_entry))
|
||||||
.on_action(cx.listener(Self::collapse_selected_entry))
|
.on_action(cx.listener(Self::collapse_selected_entry))
|
||||||
.on_action(cx.listener(Self::collapse_all_entries))
|
.on_action(cx.listener(Self::collapse_all_entries))
|
||||||
.on_action(cx.listener(Self::new_file))
|
.on_action(cx.listener(Self::open_file))
|
||||||
|
.on_action(cx.listener(Self::confirm))
|
||||||
|
.on_action(cx.listener(Self::cancel))
|
||||||
|
.on_action(cx.listener(Self::copy_path))
|
||||||
|
.on_action(cx.listener(Self::copy_relative_path))
|
||||||
|
.on_action(cx.listener(Self::new_search_in_directory))
|
||||||
|
.when(!project.is_read_only(), |el| {
|
||||||
|
el.on_action(cx.listener(Self::new_file))
|
||||||
.on_action(cx.listener(Self::new_directory))
|
.on_action(cx.listener(Self::new_directory))
|
||||||
.on_action(cx.listener(Self::rename))
|
.on_action(cx.listener(Self::rename))
|
||||||
.on_action(cx.listener(Self::delete))
|
.on_action(cx.listener(Self::delete))
|
||||||
.on_action(cx.listener(Self::confirm))
|
|
||||||
.on_action(cx.listener(Self::open_file))
|
|
||||||
.on_action(cx.listener(Self::cancel))
|
|
||||||
.on_action(cx.listener(Self::cut))
|
.on_action(cx.listener(Self::cut))
|
||||||
.on_action(cx.listener(Self::copy))
|
.on_action(cx.listener(Self::copy))
|
||||||
.on_action(cx.listener(Self::copy_path))
|
|
||||||
.on_action(cx.listener(Self::copy_relative_path))
|
|
||||||
.on_action(cx.listener(Self::paste))
|
.on_action(cx.listener(Self::paste))
|
||||||
.on_action(cx.listener(Self::reveal_in_finder))
|
})
|
||||||
|
.when(project.is_local(), |el| {
|
||||||
|
el.on_action(cx.listener(Self::reveal_in_finder))
|
||||||
.on_action(cx.listener(Self::open_in_terminal))
|
.on_action(cx.listener(Self::open_in_terminal))
|
||||||
.on_action(cx.listener(Self::new_search_in_directory))
|
})
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.child(
|
.child(
|
||||||
uniform_list(
|
uniform_list(
|
||||||
|
|
|
@ -269,6 +269,7 @@ message Participant {
|
||||||
repeated ParticipantProject projects = 3;
|
repeated ParticipantProject projects = 3;
|
||||||
ParticipantLocation location = 4;
|
ParticipantLocation location = 4;
|
||||||
uint32 participant_index = 5;
|
uint32 participant_index = 5;
|
||||||
|
ChannelRole role = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message PendingParticipant {
|
message PendingParticipant {
|
||||||
|
|
|
@ -70,7 +70,7 @@ impl BufferSearchBar {
|
||||||
fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
|
fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||||
let settings = ThemeSettings::get_global(cx);
|
let settings = ThemeSettings::get_global(cx);
|
||||||
let text_style = TextStyle {
|
let text_style = TextStyle {
|
||||||
color: if editor.read(cx).read_only() {
|
color: if editor.read(cx).read_only(cx) {
|
||||||
cx.theme().colors().text_disabled
|
cx.theme().colors().text_disabled
|
||||||
} else {
|
} else {
|
||||||
cx.theme().colors().text
|
cx.theme().colors().text
|
||||||
|
|
|
@ -132,9 +132,11 @@ pub struct ProjectSearchBar {
|
||||||
impl ProjectSearch {
|
impl ProjectSearch {
|
||||||
fn new(project: Model<Project>, cx: &mut ModelContext<Self>) -> Self {
|
fn new(project: Model<Project>, cx: &mut ModelContext<Self>) -> Self {
|
||||||
let replica_id = project.read(cx).replica_id();
|
let replica_id = project.read(cx).replica_id();
|
||||||
|
let capability = project.read(cx).capability();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
project,
|
project,
|
||||||
excerpts: cx.new_model(|_| MultiBuffer::new(replica_id)),
|
excerpts: cx.new_model(|_| MultiBuffer::new(replica_id, capability)),
|
||||||
pending_search: Default::default(),
|
pending_search: Default::default(),
|
||||||
match_ranges: Default::default(),
|
match_ranges: Default::default(),
|
||||||
active_query: None,
|
active_query: None,
|
||||||
|
@ -1556,7 +1558,7 @@ impl ProjectSearchBar {
|
||||||
fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
|
fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||||
let settings = ThemeSettings::get_global(cx);
|
let settings = ThemeSettings::get_global(cx);
|
||||||
let text_style = TextStyle {
|
let text_style = TextStyle {
|
||||||
color: if editor.read(cx).read_only() {
|
color: if editor.read(cx).read_only(cx) {
|
||||||
cx.theme().colors().text_disabled
|
cx.theme().colors().text_disabled
|
||||||
} else {
|
} else {
|
||||||
cx.theme().colors().text
|
cx.theme().colors().text
|
||||||
|
|
|
@ -131,6 +131,15 @@ impl PlayerColors {
|
||||||
*self.0.last().unwrap()
|
*self.0.last().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_only(&self) -> PlayerColor {
|
||||||
|
let local = self.local();
|
||||||
|
PlayerColor {
|
||||||
|
cursor: local.cursor.grayscale(),
|
||||||
|
background: local.background.grayscale(),
|
||||||
|
selection: local.selection.grayscale(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn color_for_participant(&self, participant_index: u32) -> PlayerColor {
|
pub fn color_for_participant(&self, participant_index: u32) -> PlayerColor {
|
||||||
let len = self.0.len() - 1;
|
let len = self.0.len() - 1;
|
||||||
self.0[(participant_index as usize % len) + 1]
|
self.0[(participant_index as usize % len) + 1]
|
||||||
|
|
|
@ -1187,7 +1187,7 @@ impl Workspace {
|
||||||
mut save_intent: SaveIntent,
|
mut save_intent: SaveIntent,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<bool>> {
|
) -> Task<Result<bool>> {
|
||||||
if self.project.read(cx).is_read_only() {
|
if self.project.read(cx).is_disconnected() {
|
||||||
return Task::ready(Ok(true));
|
return Task::ready(Ok(true));
|
||||||
}
|
}
|
||||||
let dirty_items = self
|
let dirty_items = self
|
||||||
|
@ -2510,7 +2510,7 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
|
fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let is_edited = !self.project.read(cx).is_read_only()
|
let is_edited = !self.project.read(cx).is_disconnected()
|
||||||
&& self
|
&& self
|
||||||
.items(cx)
|
.items(cx)
|
||||||
.any(|item| item.has_conflict(cx) || item.is_dirty(cx));
|
.any(|item| item.has_conflict(cx) || item.is_dirty(cx));
|
||||||
|
@ -3635,7 +3635,7 @@ impl Render for Workspace {
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(self.status_bar.clone())
|
.child(self.status_bar.clone())
|
||||||
.children(if self.project.read(cx).is_read_only() {
|
.children(if self.project.read(cx).is_disconnected() {
|
||||||
Some(DisconnectedOverlay)
|
Some(DisconnectedOverlay)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue