mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
hosted projects (#8627)
- **Allow joining a hosted project** You can't yet do anything in a hosted project, but you can join it and look how empty it is. Release Notes: - N/A
This commit is contained in:
parent
4167c66b86
commit
27c5343707
@ -1182,19 +1182,10 @@ 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 = Project::remote(
|
let project =
|
||||||
id,
|
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
|
||||||
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| {
|
||||||
|
@ -11,7 +11,7 @@ pub use channel_chat::{
|
|||||||
mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId,
|
mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId,
|
||||||
MessageParams,
|
MessageParams,
|
||||||
};
|
};
|
||||||
pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore, HostedProjectId};
|
pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod channel_store_tests;
|
mod channel_store_tests;
|
||||||
|
@ -3,7 +3,9 @@ mod channel_index;
|
|||||||
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
|
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use channel_index::ChannelIndex;
|
use channel_index::ChannelIndex;
|
||||||
use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, UserStore};
|
use client::{
|
||||||
|
ChannelId, Client, ClientSettings, HostedProjectId, Subscription, User, UserId, UserStore,
|
||||||
|
};
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
use collections::{hash_map, HashMap, HashSet};
|
||||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@ -27,9 +29,6 @@ pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppCont
|
|||||||
cx.set_global(GlobalChannelStore(channel_store));
|
cx.set_global(GlobalChannelStore(channel_store));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
|
||||||
pub struct HostedProjectId(pub u64);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
struct NotesVersion {
|
struct NotesVersion {
|
||||||
epoch: u64,
|
epoch: u64,
|
||||||
|
@ -24,6 +24,9 @@ impl std::fmt::Display for ChannelId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
|
pub struct HostedProjectId(pub u64);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct ParticipantIndex(pub u32);
|
pub struct ParticipantIndex(pub u32);
|
||||||
|
|
||||||
|
@ -46,10 +46,11 @@ CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
|
|||||||
CREATE TABLE "projects" (
|
CREATE TABLE "projects" (
|
||||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
"room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL,
|
"room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL,
|
||||||
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
|
"host_user_id" INTEGER REFERENCES users (id),
|
||||||
"host_connection_id" INTEGER,
|
"host_connection_id" INTEGER,
|
||||||
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
|
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
|
||||||
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE
|
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
"hosted_project_id" INTEGER REFERENCES hosted_projects (id)
|
||||||
);
|
);
|
||||||
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
|
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
|
||||||
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
|
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
ALTER TABLE projects ALTER COLUMN host_user_id DROP NOT NULL;
|
||||||
|
ALTER TABLE projects ADD COLUMN hosted_project_id INTEGER REFERENCES hosted_projects(id) UNIQUE NULL;
|
@ -670,6 +670,8 @@ pub struct RefreshedChannelBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
|
pub id: ProjectId,
|
||||||
|
pub role: ChannelRole,
|
||||||
pub collaborators: Vec<ProjectCollaborator>,
|
pub collaborators: Vec<ProjectCollaborator>,
|
||||||
pub worktrees: BTreeMap<u64, Worktree>,
|
pub worktrees: BTreeMap<u64, Worktree>,
|
||||||
pub language_servers: Vec<proto::LanguageServer>,
|
pub language_servers: Vec<proto::LanguageServer>,
|
||||||
@ -695,7 +697,7 @@ impl ProjectCollaborator {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct LeftProject {
|
pub struct LeftProject {
|
||||||
pub id: ProjectId,
|
pub id: ProjectId,
|
||||||
pub host_user_id: UserId,
|
pub host_user_id: Option<UserId>,
|
||||||
pub host_connection_id: Option<ConnectionId>,
|
pub host_connection_id: Option<ConnectionId>,
|
||||||
pub connection_ids: Vec<ConnectionId>,
|
pub connection_ids: Vec<ConnectionId>,
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use rpc::proto;
|
use rpc::{proto, ErrorCode};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@ -39,4 +39,44 @@ impl Database {
|
|||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_hosted_project(
|
||||||
|
&self,
|
||||||
|
hosted_project_id: HostedProjectId,
|
||||||
|
user_id: UserId,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
) -> Result<(hosted_project::Model, ChannelRole)> {
|
||||||
|
let project = hosted_project::Entity::find_by_id(hosted_project_id)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?;
|
||||||
|
let channel = channel::Entity::find_by_id(project.channel_id)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!(ErrorCode::NoSuchChannel))?;
|
||||||
|
|
||||||
|
let role = match project.visibility {
|
||||||
|
ChannelVisibility::Public => {
|
||||||
|
self.check_user_is_channel_participant(&channel, user_id, tx)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
ChannelVisibility::Members => {
|
||||||
|
self.check_user_is_channel_member(&channel, user_id, tx)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((project, role))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_hosted_project(&self, project_id: ProjectId) -> Result<bool> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
Ok(project::Entity::find_by_id(project_id)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.map(|project| project.hosted_project_id.is_some())
|
||||||
|
.ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,13 +57,14 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let project = project::ActiveModel {
|
let project = project::ActiveModel {
|
||||||
room_id: ActiveValue::set(participant.room_id),
|
room_id: ActiveValue::set(Some(participant.room_id)),
|
||||||
host_user_id: ActiveValue::set(participant.user_id),
|
host_user_id: ActiveValue::set(Some(participant.user_id)),
|
||||||
host_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
host_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||||
host_connection_server_id: ActiveValue::set(Some(ServerId(
|
host_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||||
connection.owner_id as i32,
|
connection.owner_id as i32,
|
||||||
))),
|
))),
|
||||||
..Default::default()
|
id: ActiveValue::NotSet,
|
||||||
|
hosted_project_id: ActiveValue::Set(None),
|
||||||
}
|
}
|
||||||
.insert(&*tx)
|
.insert(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
@ -153,8 +154,12 @@ impl Database {
|
|||||||
self.update_project_worktrees(project.id, worktrees, &tx)
|
self.update_project_worktrees(project.id, worktrees, &tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let room_id = project
|
||||||
|
.room_id
|
||||||
|
.ok_or_else(|| anyhow!("project not in a room"))?;
|
||||||
|
|
||||||
let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
|
let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
|
||||||
let room = self.get_room(project.room_id, &tx).await?;
|
let room = self.get_room(room_id, &tx).await?;
|
||||||
Ok((room, guest_connection_ids))
|
Ok((room, guest_connection_ids))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@ -504,8 +509,30 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds the given connection to the specified project.
|
/// Adds the given connection to the specified hosted project
|
||||||
pub async fn join_project(
|
pub async fn join_hosted_project(
|
||||||
|
&self,
|
||||||
|
id: HostedProjectId,
|
||||||
|
user_id: UserId,
|
||||||
|
connection: ConnectionId,
|
||||||
|
) -> Result<(Project, ReplicaId)> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
let (hosted_project, role) = self.get_hosted_project(id, user_id, &tx).await?;
|
||||||
|
let project = project::Entity::find()
|
||||||
|
.filter(project::Column::HostedProjectId.eq(hosted_project.id))
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("hosted project is no longer shared"))?;
|
||||||
|
|
||||||
|
self.join_project_internal(project, user_id, connection, role, &tx)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the given connection to the specified project
|
||||||
|
/// in the current room.
|
||||||
|
pub async fn join_project_in_room(
|
||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
@ -532,180 +559,240 @@ impl Database {
|
|||||||
.one(&*tx)
|
.one(&*tx)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("no such project"))?;
|
.ok_or_else(|| anyhow!("no such project"))?;
|
||||||
if project.room_id != participant.room_id {
|
if project.room_id != Some(participant.room_id) {
|
||||||
return Err(anyhow!("no such project"))?;
|
return Err(anyhow!("no such project"))?;
|
||||||
}
|
}
|
||||||
|
self.join_project_internal(
|
||||||
|
project,
|
||||||
|
participant.user_id,
|
||||||
|
connection,
|
||||||
|
participant.role.unwrap_or(ChannelRole::Member),
|
||||||
|
&tx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
let mut collaborators = project
|
async fn join_project_internal(
|
||||||
|
&self,
|
||||||
|
project: project::Model,
|
||||||
|
user_id: UserId,
|
||||||
|
connection: ConnectionId,
|
||||||
|
role: ChannelRole,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
) -> Result<(Project, ReplicaId)> {
|
||||||
|
let mut collaborators = project
|
||||||
|
.find_related(project_collaborator::Entity)
|
||||||
|
.all(&*tx)
|
||||||
|
.await?;
|
||||||
|
let replica_ids = collaborators
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.replica_id)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
let mut replica_id = ReplicaId(1);
|
||||||
|
while replica_ids.contains(&replica_id) {
|
||||||
|
replica_id.0 += 1;
|
||||||
|
}
|
||||||
|
let new_collaborator = project_collaborator::ActiveModel {
|
||||||
|
project_id: ActiveValue::set(project.id),
|
||||||
|
connection_id: ActiveValue::set(connection.id as i32),
|
||||||
|
connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
|
||||||
|
user_id: ActiveValue::set(user_id),
|
||||||
|
replica_id: ActiveValue::set(replica_id),
|
||||||
|
is_host: ActiveValue::set(false),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.insert(&*tx)
|
||||||
|
.await?;
|
||||||
|
collaborators.push(new_collaborator);
|
||||||
|
|
||||||
|
let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
|
||||||
|
let mut worktrees = db_worktrees
|
||||||
|
.into_iter()
|
||||||
|
.map(|db_worktree| {
|
||||||
|
(
|
||||||
|
db_worktree.id as u64,
|
||||||
|
Worktree {
|
||||||
|
id: db_worktree.id as u64,
|
||||||
|
abs_path: db_worktree.abs_path,
|
||||||
|
root_name: db_worktree.root_name,
|
||||||
|
visible: db_worktree.visible,
|
||||||
|
entries: Default::default(),
|
||||||
|
repository_entries: Default::default(),
|
||||||
|
diagnostic_summaries: Default::default(),
|
||||||
|
settings_files: Default::default(),
|
||||||
|
scan_id: db_worktree.scan_id as u64,
|
||||||
|
completed_scan_id: db_worktree.completed_scan_id as u64,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
|
||||||
|
// Populate worktree entries.
|
||||||
|
{
|
||||||
|
let mut db_entries = worktree_entry::Entity::find()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(worktree_entry::Column::ProjectId.eq(project.id))
|
||||||
|
.add(worktree_entry::Column::IsDeleted.eq(false)),
|
||||||
|
)
|
||||||
|
.stream(&*tx)
|
||||||
|
.await?;
|
||||||
|
while let Some(db_entry) = db_entries.next().await {
|
||||||
|
let db_entry = db_entry?;
|
||||||
|
if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) {
|
||||||
|
worktree.entries.push(proto::Entry {
|
||||||
|
id: db_entry.id as u64,
|
||||||
|
is_dir: db_entry.is_dir,
|
||||||
|
path: db_entry.path,
|
||||||
|
inode: db_entry.inode as u64,
|
||||||
|
mtime: Some(proto::Timestamp {
|
||||||
|
seconds: db_entry.mtime_seconds as u64,
|
||||||
|
nanos: db_entry.mtime_nanos as u32,
|
||||||
|
}),
|
||||||
|
is_symlink: db_entry.is_symlink,
|
||||||
|
is_ignored: db_entry.is_ignored,
|
||||||
|
is_external: db_entry.is_external,
|
||||||
|
git_status: db_entry.git_status.map(|status| status as i32),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate repository entries.
|
||||||
|
{
|
||||||
|
let mut db_repository_entries = worktree_repository::Entity::find()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(worktree_repository::Column::ProjectId.eq(project.id))
|
||||||
|
.add(worktree_repository::Column::IsDeleted.eq(false)),
|
||||||
|
)
|
||||||
|
.stream(&*tx)
|
||||||
|
.await?;
|
||||||
|
while let Some(db_repository_entry) = db_repository_entries.next().await {
|
||||||
|
let db_repository_entry = db_repository_entry?;
|
||||||
|
if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
|
||||||
|
{
|
||||||
|
worktree.repository_entries.insert(
|
||||||
|
db_repository_entry.work_directory_id as u64,
|
||||||
|
proto::RepositoryEntry {
|
||||||
|
work_directory_id: db_repository_entry.work_directory_id as u64,
|
||||||
|
branch: db_repository_entry.branch,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate worktree diagnostic summaries.
|
||||||
|
{
|
||||||
|
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
|
||||||
|
.filter(worktree_diagnostic_summary::Column::ProjectId.eq(project.id))
|
||||||
|
.stream(&*tx)
|
||||||
|
.await?;
|
||||||
|
while let Some(db_summary) = db_summaries.next().await {
|
||||||
|
let db_summary = db_summary?;
|
||||||
|
if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) {
|
||||||
|
worktree
|
||||||
|
.diagnostic_summaries
|
||||||
|
.push(proto::DiagnosticSummary {
|
||||||
|
path: db_summary.path,
|
||||||
|
language_server_id: db_summary.language_server_id as u64,
|
||||||
|
error_count: db_summary.error_count as u32,
|
||||||
|
warning_count: db_summary.warning_count as u32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate worktree settings files
|
||||||
|
{
|
||||||
|
let mut db_settings_files = worktree_settings_file::Entity::find()
|
||||||
|
.filter(worktree_settings_file::Column::ProjectId.eq(project.id))
|
||||||
|
.stream(&*tx)
|
||||||
|
.await?;
|
||||||
|
while let Some(db_settings_file) = db_settings_files.next().await {
|
||||||
|
let db_settings_file = db_settings_file?;
|
||||||
|
if let Some(worktree) = worktrees.get_mut(&(db_settings_file.worktree_id as u64)) {
|
||||||
|
worktree.settings_files.push(WorktreeSettingsFile {
|
||||||
|
path: db_settings_file.path,
|
||||||
|
content: db_settings_file.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate language servers.
|
||||||
|
let language_servers = project
|
||||||
|
.find_related(language_server::Entity)
|
||||||
|
.all(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let project = Project {
|
||||||
|
id: project.id,
|
||||||
|
role,
|
||||||
|
collaborators: collaborators
|
||||||
|
.into_iter()
|
||||||
|
.map(|collaborator| ProjectCollaborator {
|
||||||
|
connection_id: collaborator.connection(),
|
||||||
|
user_id: collaborator.user_id,
|
||||||
|
replica_id: collaborator.replica_id,
|
||||||
|
is_host: collaborator.is_host,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
worktrees,
|
||||||
|
language_servers: language_servers
|
||||||
|
.into_iter()
|
||||||
|
.map(|language_server| proto::LanguageServer {
|
||||||
|
id: language_server.id as u64,
|
||||||
|
name: language_server.name,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
Ok((project, replica_id as ReplicaId))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn leave_hosted_project(
|
||||||
|
&self,
|
||||||
|
project_id: ProjectId,
|
||||||
|
connection: ConnectionId,
|
||||||
|
) -> Result<LeftProject> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
let result = project_collaborator::Entity::delete_many()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(project_collaborator::Column::ProjectId.eq(project_id))
|
||||||
|
.add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
|
||||||
|
.add(
|
||||||
|
project_collaborator::Column::ConnectionServerId
|
||||||
|
.eq(connection.owner_id as i32),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
if result.rows_affected == 0 {
|
||||||
|
return Err(anyhow!("not in the project"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let project = project::Entity::find_by_id(project_id)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no such project"))?;
|
||||||
|
let collaborators = project
|
||||||
.find_related(project_collaborator::Entity)
|
.find_related(project_collaborator::Entity)
|
||||||
.all(&*tx)
|
.all(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
let replica_ids = collaborators
|
let connection_ids = collaborators
|
||||||
.iter()
|
|
||||||
.map(|c| c.replica_id)
|
|
||||||
.collect::<HashSet<_>>();
|
|
||||||
let mut replica_id = ReplicaId(1);
|
|
||||||
while replica_ids.contains(&replica_id) {
|
|
||||||
replica_id.0 += 1;
|
|
||||||
}
|
|
||||||
let new_collaborator = project_collaborator::ActiveModel {
|
|
||||||
project_id: ActiveValue::set(project_id),
|
|
||||||
connection_id: ActiveValue::set(connection.id as i32),
|
|
||||||
connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
|
|
||||||
user_id: ActiveValue::set(participant.user_id),
|
|
||||||
replica_id: ActiveValue::set(replica_id),
|
|
||||||
is_host: ActiveValue::set(false),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.insert(&*tx)
|
|
||||||
.await?;
|
|
||||||
collaborators.push(new_collaborator);
|
|
||||||
|
|
||||||
let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
|
|
||||||
let mut worktrees = db_worktrees
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|db_worktree| {
|
.map(|collaborator| collaborator.connection())
|
||||||
(
|
.collect();
|
||||||
db_worktree.id as u64,
|
Ok(LeftProject {
|
||||||
Worktree {
|
id: project.id,
|
||||||
id: db_worktree.id as u64,
|
connection_ids,
|
||||||
abs_path: db_worktree.abs_path,
|
host_user_id: None,
|
||||||
root_name: db_worktree.root_name,
|
host_connection_id: None,
|
||||||
visible: db_worktree.visible,
|
})
|
||||||
entries: Default::default(),
|
|
||||||
repository_entries: Default::default(),
|
|
||||||
diagnostic_summaries: Default::default(),
|
|
||||||
settings_files: Default::default(),
|
|
||||||
scan_id: db_worktree.scan_id as u64,
|
|
||||||
completed_scan_id: db_worktree.completed_scan_id as u64,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<BTreeMap<_, _>>();
|
|
||||||
|
|
||||||
// Populate worktree entries.
|
|
||||||
{
|
|
||||||
let mut db_entries = worktree_entry::Entity::find()
|
|
||||||
.filter(
|
|
||||||
Condition::all()
|
|
||||||
.add(worktree_entry::Column::ProjectId.eq(project_id))
|
|
||||||
.add(worktree_entry::Column::IsDeleted.eq(false)),
|
|
||||||
)
|
|
||||||
.stream(&*tx)
|
|
||||||
.await?;
|
|
||||||
while let Some(db_entry) = db_entries.next().await {
|
|
||||||
let db_entry = db_entry?;
|
|
||||||
if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) {
|
|
||||||
worktree.entries.push(proto::Entry {
|
|
||||||
id: db_entry.id as u64,
|
|
||||||
is_dir: db_entry.is_dir,
|
|
||||||
path: db_entry.path,
|
|
||||||
inode: db_entry.inode as u64,
|
|
||||||
mtime: Some(proto::Timestamp {
|
|
||||||
seconds: db_entry.mtime_seconds as u64,
|
|
||||||
nanos: db_entry.mtime_nanos as u32,
|
|
||||||
}),
|
|
||||||
is_symlink: db_entry.is_symlink,
|
|
||||||
is_ignored: db_entry.is_ignored,
|
|
||||||
is_external: db_entry.is_external,
|
|
||||||
git_status: db_entry.git_status.map(|status| status as i32),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate repository entries.
|
|
||||||
{
|
|
||||||
let mut db_repository_entries = worktree_repository::Entity::find()
|
|
||||||
.filter(
|
|
||||||
Condition::all()
|
|
||||||
.add(worktree_repository::Column::ProjectId.eq(project_id))
|
|
||||||
.add(worktree_repository::Column::IsDeleted.eq(false)),
|
|
||||||
)
|
|
||||||
.stream(&*tx)
|
|
||||||
.await?;
|
|
||||||
while let Some(db_repository_entry) = db_repository_entries.next().await {
|
|
||||||
let db_repository_entry = db_repository_entry?;
|
|
||||||
if let Some(worktree) =
|
|
||||||
worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
|
|
||||||
{
|
|
||||||
worktree.repository_entries.insert(
|
|
||||||
db_repository_entry.work_directory_id as u64,
|
|
||||||
proto::RepositoryEntry {
|
|
||||||
work_directory_id: db_repository_entry.work_directory_id as u64,
|
|
||||||
branch: db_repository_entry.branch,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate worktree diagnostic summaries.
|
|
||||||
{
|
|
||||||
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
|
|
||||||
.filter(worktree_diagnostic_summary::Column::ProjectId.eq(project_id))
|
|
||||||
.stream(&*tx)
|
|
||||||
.await?;
|
|
||||||
while let Some(db_summary) = db_summaries.next().await {
|
|
||||||
let db_summary = db_summary?;
|
|
||||||
if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) {
|
|
||||||
worktree
|
|
||||||
.diagnostic_summaries
|
|
||||||
.push(proto::DiagnosticSummary {
|
|
||||||
path: db_summary.path,
|
|
||||||
language_server_id: db_summary.language_server_id as u64,
|
|
||||||
error_count: db_summary.error_count as u32,
|
|
||||||
warning_count: db_summary.warning_count as u32,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate worktree settings files
|
|
||||||
{
|
|
||||||
let mut db_settings_files = worktree_settings_file::Entity::find()
|
|
||||||
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
|
|
||||||
.stream(&*tx)
|
|
||||||
.await?;
|
|
||||||
while let Some(db_settings_file) = db_settings_files.next().await {
|
|
||||||
let db_settings_file = db_settings_file?;
|
|
||||||
if let Some(worktree) =
|
|
||||||
worktrees.get_mut(&(db_settings_file.worktree_id as u64))
|
|
||||||
{
|
|
||||||
worktree.settings_files.push(WorktreeSettingsFile {
|
|
||||||
path: db_settings_file.path,
|
|
||||||
content: db_settings_file.content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate language servers.
|
|
||||||
let language_servers = project
|
|
||||||
.find_related(language_server::Entity)
|
|
||||||
.all(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let project = Project {
|
|
||||||
collaborators: collaborators
|
|
||||||
.into_iter()
|
|
||||||
.map(|collaborator| ProjectCollaborator {
|
|
||||||
connection_id: collaborator.connection(),
|
|
||||||
user_id: collaborator.user_id,
|
|
||||||
replica_id: collaborator.replica_id,
|
|
||||||
is_host: collaborator.is_host,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
worktrees,
|
|
||||||
language_servers: language_servers
|
|
||||||
.into_iter()
|
|
||||||
.map(|language_server| proto::LanguageServer {
|
|
||||||
id: language_server.id as u64,
|
|
||||||
name: language_server.name,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
};
|
|
||||||
Ok((project, replica_id as ReplicaId))
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@ -772,7 +859,7 @@ impl Database {
|
|||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let room = self.get_room(project.room_id, &tx).await?;
|
let room = self.get_room(room_id, &tx).await?;
|
||||||
let left_project = LeftProject {
|
let left_project = LeftProject {
|
||||||
id: project_id,
|
id: project_id,
|
||||||
host_user_id: project.host_user_id,
|
host_user_id: project.host_user_id,
|
||||||
@ -996,7 +1083,9 @@ impl Database {
|
|||||||
.one(&*tx)
|
.one(&*tx)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("project {} not found", project_id))?;
|
.ok_or_else(|| anyhow!("project {} not found", project_id))?;
|
||||||
Ok(project.room_id)
|
Ok(project
|
||||||
|
.room_id
|
||||||
|
.ok_or_else(|| anyhow!("project not in room"))?)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -491,7 +491,7 @@ impl Database {
|
|||||||
.one(&*tx)
|
.one(&*tx)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("project does not exist"))?;
|
.ok_or_else(|| anyhow!("project does not exist"))?;
|
||||||
if project.host_user_id != user_id {
|
if project.host_user_id != Some(user_id) {
|
||||||
return Err(anyhow!("no such project"))?;
|
return Err(anyhow!("no such project"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -851,7 +851,7 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if collaborator.is_host {
|
if collaborator.is_host {
|
||||||
left_project.host_user_id = collaborator.user_id;
|
left_project.host_user_id = Some(collaborator.user_id);
|
||||||
left_project.host_connection_id = Some(collaborator_connection_id);
|
left_project.host_connection_id = Some(collaborator_connection_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::db::{ProjectId, Result, RoomId, ServerId, UserId};
|
use crate::db::{HostedProjectId, ProjectId, Result, RoomId, ServerId, UserId};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use rpc::ConnectionId;
|
use rpc::ConnectionId;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
@ -8,10 +8,11 @@ use sea_orm::entity::prelude::*;
|
|||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: ProjectId,
|
pub id: ProjectId,
|
||||||
pub room_id: RoomId,
|
pub room_id: Option<RoomId>,
|
||||||
pub host_user_id: UserId,
|
pub host_user_id: Option<UserId>,
|
||||||
pub host_connection_id: Option<i32>,
|
pub host_connection_id: Option<i32>,
|
||||||
pub host_connection_server_id: Option<ServerId>,
|
pub host_connection_server_id: Option<ServerId>,
|
||||||
|
pub hosted_project_id: Option<HostedProjectId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
|
@ -4,8 +4,9 @@ use crate::{
|
|||||||
auth::{self, Impersonator},
|
auth::{self, Impersonator},
|
||||||
db::{
|
db::{
|
||||||
self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database,
|
self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database,
|
||||||
InviteMemberResult, MembershipUpdated, MessageId, NotificationId, ProjectId,
|
HostedProjectId, InviteMemberResult, MembershipUpdated, MessageId, NotificationId, Project,
|
||||||
RemoveChannelMemberResult, RespondToChannelInvite, RoomId, ServerId, User, UserId,
|
ProjectId, RemoveChannelMemberResult, ReplicaId, RespondToChannelInvite, RoomId, ServerId,
|
||||||
|
User, UserId,
|
||||||
},
|
},
|
||||||
executor::Executor,
|
executor::Executor,
|
||||||
AppState, Error, Result,
|
AppState, Error, Result,
|
||||||
@ -197,6 +198,7 @@ impl Server {
|
|||||||
.add_request_handler(share_project)
|
.add_request_handler(share_project)
|
||||||
.add_message_handler(unshare_project)
|
.add_message_handler(unshare_project)
|
||||||
.add_request_handler(join_project)
|
.add_request_handler(join_project)
|
||||||
|
.add_request_handler(join_hosted_project)
|
||||||
.add_message_handler(leave_project)
|
.add_message_handler(leave_project)
|
||||||
.add_request_handler(update_project)
|
.add_request_handler(update_project)
|
||||||
.add_request_handler(update_worktree)
|
.add_request_handler(update_worktree)
|
||||||
@ -1584,22 +1586,46 @@ async fn join_project(
|
|||||||
session: Session,
|
session: Session,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.project_id);
|
let project_id = ProjectId::from_proto(request.project_id);
|
||||||
let guest_user_id = session.user_id;
|
|
||||||
|
|
||||||
tracing::info!(%project_id, "join project");
|
tracing::info!(%project_id, "join project");
|
||||||
|
|
||||||
let (project, replica_id) = &mut *session
|
let (project, replica_id) = &mut *session
|
||||||
.db()
|
.db()
|
||||||
.await
|
.await
|
||||||
.join_project(project_id, session.connection_id)
|
.join_project_in_room(project_id, session.connection_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
join_project_internal(response, session, project, replica_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
trait JoinProjectInternalResponse {
|
||||||
|
fn send(self, result: proto::JoinProjectResponse) -> Result<()>;
|
||||||
|
}
|
||||||
|
impl JoinProjectInternalResponse for Response<proto::JoinProject> {
|
||||||
|
fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
|
||||||
|
Response::<proto::JoinProject>::send(self, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl JoinProjectInternalResponse for Response<proto::JoinHostedProject> {
|
||||||
|
fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
|
||||||
|
Response::<proto::JoinHostedProject>::send(self, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join_project_internal(
|
||||||
|
response: impl JoinProjectInternalResponse,
|
||||||
|
session: Session,
|
||||||
|
project: &mut Project,
|
||||||
|
replica_id: &ReplicaId,
|
||||||
|
) -> Result<()> {
|
||||||
let collaborators = project
|
let collaborators = project
|
||||||
.collaborators
|
.collaborators
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|collaborator| collaborator.connection_id != session.connection_id)
|
.filter(|collaborator| collaborator.connection_id != session.connection_id)
|
||||||
.map(|collaborator| collaborator.to_proto())
|
.map(|collaborator| collaborator.to_proto())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
let project_id = project.id;
|
||||||
|
let guest_user_id = session.user_id;
|
||||||
|
|
||||||
let worktrees = project
|
let worktrees = project
|
||||||
.worktrees
|
.worktrees
|
||||||
@ -1631,10 +1657,12 @@ async fn join_project(
|
|||||||
|
|
||||||
// First, we send the metadata associated with each worktree.
|
// First, we send the metadata associated with each worktree.
|
||||||
response.send(proto::JoinProjectResponse {
|
response.send(proto::JoinProjectResponse {
|
||||||
|
project_id: project.id.0 as u64,
|
||||||
worktrees: worktrees.clone(),
|
worktrees: worktrees.clone(),
|
||||||
replica_id: replica_id.0 as u32,
|
replica_id: replica_id.0 as u32,
|
||||||
collaborators: collaborators.clone(),
|
collaborators: collaborators.clone(),
|
||||||
language_servers: project.language_servers.clone(),
|
language_servers: project.language_servers.clone(),
|
||||||
|
role: project.role.into(), // todo
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
for (worktree_id, worktree) in mem::take(&mut project.worktrees) {
|
for (worktree_id, worktree) in mem::take(&mut project.worktrees) {
|
||||||
@ -1707,15 +1735,17 @@ async fn join_project(
|
|||||||
async fn leave_project(request: proto::LeaveProject, session: Session) -> Result<()> {
|
async fn leave_project(request: proto::LeaveProject, session: Session) -> Result<()> {
|
||||||
let sender_id = session.connection_id;
|
let sender_id = session.connection_id;
|
||||||
let project_id = ProjectId::from_proto(request.project_id);
|
let project_id = ProjectId::from_proto(request.project_id);
|
||||||
|
let db = session.db().await;
|
||||||
|
if db.is_hosted_project(project_id).await? {
|
||||||
|
let project = db.leave_hosted_project(project_id, sender_id).await?;
|
||||||
|
project_left(&project, &session);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let (room, project) = &*session
|
let (room, project) = &*db.leave_project(project_id, sender_id).await?;
|
||||||
.db()
|
|
||||||
.await
|
|
||||||
.leave_project(project_id, sender_id)
|
|
||||||
.await?;
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
%project_id,
|
%project_id,
|
||||||
host_user_id = %project.host_user_id,
|
host_user_id = ?project.host_user_id,
|
||||||
host_connection_id = ?project.host_connection_id,
|
host_connection_id = ?project.host_connection_id,
|
||||||
"leave project"
|
"leave project"
|
||||||
);
|
);
|
||||||
@ -1726,6 +1756,24 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn join_hosted_project(
|
||||||
|
request: proto::JoinHostedProject,
|
||||||
|
response: Response<proto::JoinHostedProject>,
|
||||||
|
session: Session,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (mut project, replica_id) = session
|
||||||
|
.db()
|
||||||
|
.await
|
||||||
|
.join_hosted_project(
|
||||||
|
HostedProjectId(request.id as i32),
|
||||||
|
session.user_id,
|
||||||
|
session.connection_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
join_project_internal(response, session, &mut project, &replica_id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Updates other participants with changes to the project
|
/// Updates other participants with changes to the project
|
||||||
async fn update_project(
|
async fn update_project(
|
||||||
request: proto::UpdateProject,
|
request: proto::UpdateProject,
|
||||||
@ -3624,7 +3672,7 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
|
|||||||
|
|
||||||
fn project_left(project: &db::LeftProject, session: &Session) {
|
fn project_left(project: &db::LeftProject, session: &Session) {
|
||||||
for connection_id in &project.connection_ids {
|
for connection_id in &project.connection_ids {
|
||||||
if project.host_user_id == session.user_id {
|
if project.host_user_id == Some(session.user_id) {
|
||||||
session
|
session
|
||||||
.peer
|
.peer
|
||||||
.send(
|
.send(
|
||||||
|
@ -1534,7 +1534,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
|||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
assert_eq!(visible_push_notifications(cx_a).len(), 1);
|
assert_eq!(visible_push_notifications(cx_a).len(), 1);
|
||||||
cx_a.update(|cx| {
|
cx_a.update(|cx| {
|
||||||
workspace::join_remote_project(
|
workspace::join_in_room_project(
|
||||||
project_b_id,
|
project_b_id,
|
||||||
client_b.user_id().unwrap(),
|
client_b.user_id().unwrap(),
|
||||||
client_a.app_state.clone(),
|
client_a.app_state.clone(),
|
||||||
|
@ -22,7 +22,6 @@ 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::{
|
||||||
@ -3742,7 +3741,6 @@ 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,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -7,8 +7,8 @@ use crate::{
|
|||||||
CollaborationPanelSettings,
|
CollaborationPanelSettings,
|
||||||
};
|
};
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use channel::{Channel, ChannelEvent, ChannelStore, HostedProjectId};
|
use channel::{Channel, ChannelEvent, ChannelStore};
|
||||||
use client::{ChannelId, Client, Contact, User, UserStore};
|
use client::{ChannelId, Client, Contact, HostedProjectId, User, UserStore};
|
||||||
use contact_finder::ContactFinder;
|
use contact_finder::ContactFinder;
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::{Editor, EditorElement, EditorStyle};
|
use editor::{Editor, EditorElement, EditorStyle};
|
||||||
@ -911,7 +911,7 @@ impl CollabPanel {
|
|||||||
this.workspace
|
this.workspace
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
let app_state = workspace.app_state().clone();
|
let app_state = workspace.app_state().clone();
|
||||||
workspace::join_remote_project(project_id, host_user_id, app_state, cx)
|
workspace::join_in_room_project(project_id, host_user_id, app_state, cx)
|
||||||
.detach_and_prompt_err("Failed to join project", cx, |_, _| None);
|
.detach_and_prompt_err("Failed to join project", cx, |_, _| None);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
@ -1047,8 +1047,15 @@ impl CollabPanel {
|
|||||||
.indent_level(2)
|
.indent_level(2)
|
||||||
.indent_step_size(px(20.))
|
.indent_step_size(px(20.))
|
||||||
.selected(is_selected)
|
.selected(is_selected)
|
||||||
.on_click(cx.listener(move |_this, _, _cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
// todo()
|
if let Some(workspace) = this.workspace.upgrade() {
|
||||||
|
let app_state = workspace.read(cx).app_state().clone();
|
||||||
|
workspace::join_hosted_project(id, app_state, cx).detach_and_prompt_err(
|
||||||
|
"Failed to open project",
|
||||||
|
cx,
|
||||||
|
|_, _| None,
|
||||||
|
)
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
.start_slot(
|
.start_slot(
|
||||||
h_flex()
|
h_flex()
|
||||||
@ -1461,7 +1468,7 @@ impl CollabPanel {
|
|||||||
} => {
|
} => {
|
||||||
if let Some(workspace) = self.workspace.upgrade() {
|
if let Some(workspace) = self.workspace.upgrade() {
|
||||||
let app_state = workspace.read(cx).app_state().clone();
|
let app_state = workspace.read(cx).app_state().clone();
|
||||||
workspace::join_remote_project(
|
workspace::join_in_room_project(
|
||||||
*project_id,
|
*project_id,
|
||||||
*host_user_id,
|
*host_user_id,
|
||||||
app_state,
|
app_state,
|
||||||
|
@ -82,7 +82,7 @@ impl IncomingCallNotificationState {
|
|||||||
if let Some(project_id) = initial_project_id {
|
if let Some(project_id) = initial_project_id {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
if let Some(app_state) = app_state.upgrade() {
|
if let Some(app_state) = app_state.upgrade() {
|
||||||
workspace::join_remote_project(
|
workspace::join_in_room_project(
|
||||||
project_id,
|
project_id,
|
||||||
caller_user_id,
|
caller_user_id,
|
||||||
app_state,
|
app_state,
|
||||||
|
@ -98,7 +98,7 @@ impl ProjectSharedNotification {
|
|||||||
|
|
||||||
fn join(&mut self, cx: &mut ViewContext<Self>) {
|
fn join(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(app_state) = self.app_state.upgrade() {
|
if let Some(app_state) = self.app_state.upgrade() {
|
||||||
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
|
workspace::join_in_room_project(self.project_id, self.owner.id, app_state, cx)
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ mod project_tests;
|
|||||||
|
|
||||||
use anyhow::{anyhow, bail, Context as _, Result};
|
use anyhow::{anyhow, bail, Context as _, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
|
use client::{proto, Client, Collaborator, HostedProjectId, TypedEnvelope, UserStore};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
|
use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
|
||||||
use copilot::Copilot;
|
use copilot::Copilot;
|
||||||
@ -167,6 +167,7 @@ pub struct Project {
|
|||||||
prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
|
prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
|
||||||
prettier_instances: HashMap<PathBuf, PrettierInstance>,
|
prettier_instances: HashMap<PathBuf, PrettierInstance>,
|
||||||
tasks: Model<Inventory>,
|
tasks: Model<Inventory>,
|
||||||
|
hosted_project_id: Option<HostedProjectId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum LanguageServerToQuery {
|
pub enum LanguageServerToQuery {
|
||||||
@ -605,6 +606,7 @@ impl Project {
|
|||||||
prettiers_per_worktree: HashMap::default(),
|
prettiers_per_worktree: HashMap::default(),
|
||||||
prettier_instances: HashMap::default(),
|
prettier_instances: HashMap::default(),
|
||||||
tasks,
|
tasks,
|
||||||
|
hosted_project_id: None,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -615,17 +617,30 @@ 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,
|
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?;
|
||||||
|
|
||||||
let subscription = client.subscribe_to_entity(remote_id)?;
|
|
||||||
let response = client
|
let response = client
|
||||||
.request_envelope(proto::JoinProject {
|
.request_envelope(proto::JoinProject {
|
||||||
project_id: remote_id,
|
project_id: remote_id,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
Self::from_join_project_response(response, None, client, user_store, languages, fs, cx)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
async fn from_join_project_response(
|
||||||
|
response: TypedEnvelope<proto::JoinProjectResponse>,
|
||||||
|
hosted_project_id: Option<HostedProjectId>,
|
||||||
|
client: Arc<Client>,
|
||||||
|
user_store: Model<UserStore>,
|
||||||
|
languages: Arc<LanguageRegistry>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<Model<Self>> {
|
||||||
|
let remote_id = response.payload.project_id;
|
||||||
|
let role = response.payload.role();
|
||||||
|
let subscription = client.subscribe_to_entity(remote_id)?;
|
||||||
let this = cx.new_model(|cx| {
|
let this = cx.new_model(|cx| {
|
||||||
let replica_id = response.payload.replica_id as ReplicaId;
|
let replica_id = response.payload.replica_id as ReplicaId;
|
||||||
let tasks = Inventory::new(cx);
|
let tasks = Inventory::new(cx);
|
||||||
@ -714,6 +729,7 @@ impl Project {
|
|||||||
prettiers_per_worktree: HashMap::default(),
|
prettiers_per_worktree: HashMap::default(),
|
||||||
prettier_instances: HashMap::default(),
|
prettier_instances: HashMap::default(),
|
||||||
tasks,
|
tasks,
|
||||||
|
hosted_project_id,
|
||||||
};
|
};
|
||||||
this.set_role(role, cx);
|
this.set_role(role, cx);
|
||||||
for worktree in worktrees {
|
for worktree in worktrees {
|
||||||
@ -742,6 +758,31 @@ impl Project {
|
|||||||
Ok(this)
|
Ok(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn hosted(
|
||||||
|
hosted_project_id: HostedProjectId,
|
||||||
|
user_store: Model<UserStore>,
|
||||||
|
client: Arc<Client>,
|
||||||
|
languages: Arc<LanguageRegistry>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
cx: AsyncAppContext,
|
||||||
|
) -> Result<Model<Self>> {
|
||||||
|
let response = client
|
||||||
|
.request_envelope(proto::JoinHostedProject {
|
||||||
|
id: hosted_project_id.0,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Self::from_join_project_response(
|
||||||
|
response,
|
||||||
|
Some(hosted_project_id),
|
||||||
|
client,
|
||||||
|
user_store,
|
||||||
|
languages,
|
||||||
|
fs,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
fn release(&mut self, cx: &mut AppContext) {
|
fn release(&mut self, cx: &mut AppContext) {
|
||||||
match &self.client_state {
|
match &self.client_state {
|
||||||
ProjectClientState::Local => {}
|
ProjectClientState::Local => {}
|
||||||
@ -987,6 +1028,10 @@ impl Project {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hosted_project_id(&self) -> Option<HostedProjectId> {
|
||||||
|
self.hosted_project_id
|
||||||
|
}
|
||||||
|
|
||||||
pub fn replica_id(&self) -> ReplicaId {
|
pub fn replica_id(&self) -> ReplicaId {
|
||||||
match self.client_state {
|
match self.client_state {
|
||||||
ProjectClientState::Remote { replica_id, .. } => replica_id,
|
ProjectClientState::Remote { replica_id, .. } => replica_id,
|
||||||
|
@ -196,6 +196,8 @@ message Envelope {
|
|||||||
|
|
||||||
GetImplementation get_implementation = 162;
|
GetImplementation get_implementation = 162;
|
||||||
GetImplementationResponse get_implementation_response = 163;
|
GetImplementationResponse get_implementation_response = 163;
|
||||||
|
|
||||||
|
JoinHostedProject join_hosted_project = 164;
|
||||||
}
|
}
|
||||||
|
|
||||||
reserved 158 to 161;
|
reserved 158 to 161;
|
||||||
@ -230,6 +232,7 @@ enum ErrorCode {
|
|||||||
CircularNesting = 10;
|
CircularNesting = 10;
|
||||||
WrongMoveTarget = 11;
|
WrongMoveTarget = 11;
|
||||||
UnsharedItem = 12;
|
UnsharedItem = 12;
|
||||||
|
NoSuchProject = 13;
|
||||||
reserved 6;
|
reserved 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,11 +407,17 @@ message JoinProject {
|
|||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message JoinHostedProject {
|
||||||
|
uint64 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message JoinProjectResponse {
|
message JoinProjectResponse {
|
||||||
|
uint64 project_id = 5;
|
||||||
uint32 replica_id = 1;
|
uint32 replica_id = 1;
|
||||||
repeated WorktreeMetadata worktrees = 2;
|
repeated WorktreeMetadata worktrees = 2;
|
||||||
repeated Collaborator collaborators = 3;
|
repeated Collaborator collaborators = 3;
|
||||||
repeated LanguageServer language_servers = 4;
|
repeated LanguageServer language_servers = 4;
|
||||||
|
ChannelRole role = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LeaveProject {
|
message LeaveProject {
|
||||||
|
@ -206,6 +206,7 @@ messages!(
|
|||||||
(JoinChannelChat, Foreground),
|
(JoinChannelChat, Foreground),
|
||||||
(JoinChannelChatResponse, Foreground),
|
(JoinChannelChatResponse, Foreground),
|
||||||
(JoinProject, Foreground),
|
(JoinProject, Foreground),
|
||||||
|
(JoinHostedProject, Foreground),
|
||||||
(JoinProjectResponse, Foreground),
|
(JoinProjectResponse, Foreground),
|
||||||
(JoinRoom, Foreground),
|
(JoinRoom, Foreground),
|
||||||
(JoinRoomResponse, Foreground),
|
(JoinRoomResponse, Foreground),
|
||||||
@ -329,6 +330,7 @@ request_messages!(
|
|||||||
(JoinChannel, JoinRoomResponse),
|
(JoinChannel, JoinRoomResponse),
|
||||||
(JoinChannelBuffer, JoinChannelBufferResponse),
|
(JoinChannelBuffer, JoinChannelBufferResponse),
|
||||||
(JoinChannelChat, JoinChannelChatResponse),
|
(JoinChannelChat, JoinChannelChatResponse),
|
||||||
|
(JoinHostedProject, JoinProjectResponse),
|
||||||
(JoinProject, JoinProjectResponse),
|
(JoinProject, JoinProjectResponse),
|
||||||
(JoinRoom, JoinRoomResponse),
|
(JoinRoom, JoinRoomResponse),
|
||||||
(LeaveChannelBuffer, Ack),
|
(LeaveChannelBuffer, Ack),
|
||||||
|
@ -268,7 +268,7 @@ impl Member {
|
|||||||
this.cursor_pointer().on_mouse_down(
|
this.cursor_pointer().on_mouse_down(
|
||||||
MouseButton::Left,
|
MouseButton::Left,
|
||||||
cx.listener(move |this, _, cx| {
|
cx.listener(move |this, _, cx| {
|
||||||
crate::join_remote_project(
|
crate::join_in_room_project(
|
||||||
leader_project_id,
|
leader_project_id,
|
||||||
leader_user_id,
|
leader_user_id,
|
||||||
this.app_state().clone(),
|
this.app_state().clone(),
|
||||||
|
@ -15,7 +15,7 @@ use anyhow::{anyhow, Context as _, Result};
|
|||||||
use call::{call_settings::CallSettings, ActiveCall};
|
use call::{call_settings::CallSettings, ActiveCall};
|
||||||
use client::{
|
use client::{
|
||||||
proto::{self, ErrorCode, PeerId},
|
proto::{self, ErrorCode, PeerId},
|
||||||
ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
|
ChannelId, Client, ErrorExt, HostedProjectId, Status, TypedEnvelope, UserStore,
|
||||||
};
|
};
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
use collections::{hash_map, HashMap, HashSet};
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
@ -2635,7 +2635,7 @@ impl Workspace {
|
|||||||
// if they are active in another project, follow there.
|
// if they are active in another project, follow there.
|
||||||
if let Some(project_id) = other_project_id {
|
if let Some(project_id) = other_project_id {
|
||||||
let app_state = self.app_state.clone();
|
let app_state = self.app_state.clone();
|
||||||
crate::join_remote_project(project_id, remote_participant.user.id, app_state, cx)
|
crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4158,7 +4158,7 @@ async fn join_channel_internal(
|
|||||||
if let Some(room) = open_room {
|
if let Some(room) = open_room {
|
||||||
let task = room.update(cx, |room, cx| {
|
let task = room.update(cx, |room, cx| {
|
||||||
if let Some((project, host)) = room.most_active_project(cx) {
|
if let Some((project, host)) = room.most_active_project(cx) {
|
||||||
return Some(join_remote_project(project, host, app_state.clone(), cx));
|
return Some(join_in_room_project(project, host, app_state.clone(), cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
@ -4229,7 +4229,7 @@ async fn join_channel_internal(
|
|||||||
|
|
||||||
let task = room.update(cx, |room, cx| {
|
let task = room.update(cx, |room, cx| {
|
||||||
if let Some((project, host)) = room.most_active_project(cx) {
|
if let Some((project, host)) = room.most_active_project(cx) {
|
||||||
return Some(join_remote_project(project, host, app_state.clone(), cx));
|
return Some(join_in_room_project(project, host, app_state.clone(), cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
// if you are the first to join a channel, share your project
|
// if you are the first to join a channel, share your project
|
||||||
@ -4464,7 +4464,56 @@ pub fn create_and_open_local_file(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn join_remote_project(
|
pub fn join_hosted_project(
|
||||||
|
hosted_project_id: HostedProjectId,
|
||||||
|
app_state: Arc<AppState>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let existing_window = cx.update(|cx| {
|
||||||
|
cx.windows().into_iter().find_map(|window| {
|
||||||
|
let workspace = window.downcast::<Workspace>()?;
|
||||||
|
workspace
|
||||||
|
.read(cx)
|
||||||
|
.is_ok_and(|workspace| {
|
||||||
|
workspace.project().read(cx).hosted_project_id() == Some(hosted_project_id)
|
||||||
|
})
|
||||||
|
.then(|| workspace)
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let workspace = if let Some(existing_window) = existing_window {
|
||||||
|
existing_window
|
||||||
|
} else {
|
||||||
|
let project = Project::hosted(
|
||||||
|
hosted_project_id,
|
||||||
|
app_state.user_store.clone(),
|
||||||
|
app_state.client.clone(),
|
||||||
|
app_state.languages.clone(),
|
||||||
|
app_state.fs.clone(),
|
||||||
|
cx.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let window_bounds_override = window_bounds_env_override(&cx);
|
||||||
|
cx.update(|cx| {
|
||||||
|
let options = (app_state.build_window_options)(window_bounds_override, None, cx);
|
||||||
|
cx.open_window(options, |cx| {
|
||||||
|
cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx))
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
|
workspace.update(&mut cx, |_, cx| {
|
||||||
|
cx.activate(true);
|
||||||
|
cx.activate_window();
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn join_in_room_project(
|
||||||
project_id: u64,
|
project_id: u64,
|
||||||
follow_user_id: u64,
|
follow_user_id: u64,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
|
Loading…
Reference in New Issue
Block a user