mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-21 03:18:47 +03:00
remote projects per user (#10594)
Release Notes: - Made remote projects per-user instead of per-channel. If you'd like to be part of the remote development alpha, please email hi@zed.dev. --------- Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> Co-authored-by: Bennet <bennetbo@gmx.de> Co-authored-by: Nate Butler <1714999+iamnbutler@users.noreply.github.com> Co-authored-by: Nate Butler <iamnbutler@gmail.com>
This commit is contained in:
parent
8ae4c3277f
commit
e0c83a1d32
23
Cargo.lock
generated
23
Cargo.lock
generated
@ -2254,6 +2254,7 @@ dependencies = [
|
|||||||
"prost",
|
"prost",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"release_channel",
|
"release_channel",
|
||||||
|
"remote_projects",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rpc",
|
"rpc",
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
@ -2299,7 +2300,6 @@ dependencies = [
|
|||||||
"editor",
|
"editor",
|
||||||
"emojis",
|
"emojis",
|
||||||
"extensions_ui",
|
"extensions_ui",
|
||||||
"feature_flags",
|
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
@ -7728,7 +7728,9 @@ dependencies = [
|
|||||||
name = "recent_projects"
|
name = "recent_projects"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"editor",
|
"editor",
|
||||||
|
"feature_flags",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
@ -7736,10 +7738,15 @@ dependencies = [
|
|||||||
"ordered-float 2.10.0",
|
"ordered-float 2.10.0",
|
||||||
"picker",
|
"picker",
|
||||||
"project",
|
"project",
|
||||||
|
"remote_projects",
|
||||||
|
"rpc",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"settings",
|
||||||
"smol",
|
"smol",
|
||||||
|
"theme",
|
||||||
"ui",
|
"ui",
|
||||||
|
"ui_text_field",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
@ -7866,6 +7873,18 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "remote_projects"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"client",
|
||||||
|
"gpui",
|
||||||
|
"rpc",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rend"
|
name = "rend"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -12303,6 +12322,7 @@ dependencies = [
|
|||||||
"parking_lot",
|
"parking_lot",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
|
"remote_projects",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -12601,6 +12621,7 @@ dependencies = [
|
|||||||
"quick_action_bar",
|
"quick_action_bar",
|
||||||
"recent_projects",
|
"recent_projects",
|
||||||
"release_channel",
|
"release_channel",
|
||||||
|
"remote_projects",
|
||||||
"rope",
|
"rope",
|
||||||
"search",
|
"search",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -67,6 +67,7 @@ members = [
|
|||||||
"crates/refineable",
|
"crates/refineable",
|
||||||
"crates/refineable/derive_refineable",
|
"crates/refineable/derive_refineable",
|
||||||
"crates/release_channel",
|
"crates/release_channel",
|
||||||
|
"crates/remote_projects",
|
||||||
"crates/rich_text",
|
"crates/rich_text",
|
||||||
"crates/rope",
|
"crates/rope",
|
||||||
"crates/rpc",
|
"crates/rpc",
|
||||||
@ -200,6 +201,7 @@ project_symbols = { path = "crates/project_symbols" }
|
|||||||
quick_action_bar = { path = "crates/quick_action_bar" }
|
quick_action_bar = { path = "crates/quick_action_bar" }
|
||||||
recent_projects = { path = "crates/recent_projects" }
|
recent_projects = { path = "crates/recent_projects" }
|
||||||
release_channel = { path = "crates/release_channel" }
|
release_channel = { path = "crates/release_channel" }
|
||||||
|
remote_projects = { path = "crates/remote_projects" }
|
||||||
rich_text = { path = "crates/rich_text" }
|
rich_text = { path = "crates/rich_text" }
|
||||||
rope = { path = "crates/rope" }
|
rope = { path = "crates/rope" }
|
||||||
rpc = { path = "crates/rpc" }
|
rpc = { path = "crates/rpc" }
|
||||||
|
@ -1,5 +1,16 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<path d="M7.99993 6.85713C11.1558 6.85713 13.7142 5.83379 13.7142 4.57142C13.7142 3.30905 11.1558 2.28571 7.99993 2.28571C4.84402 2.28571 2.28564 3.30905 2.28564 4.57142C2.28564 5.83379 4.84402 6.85713 7.99993 6.85713Z" fill="black" stroke="black" stroke-width="1.5"/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path d="M13.7142 4.57141V11.4286C13.7142 12.691 11.1558 13.7143 7.99993 13.7143C4.84402 13.7143 2.28564 12.691 2.28564 11.4286V4.57141" stroke="black" stroke-width="1.5"/>
|
width="24"
|
||||||
<path d="M13.7142 8C13.7142 9.26237 11.1558 10.2857 7.99993 10.2857C4.84402 10.2857 2.28564 9.26237 2.28564 8" stroke="black" stroke-width="1.5"/>
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
|
||||||
|
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
|
||||||
|
<line x1="6" x2="6.01" y1="6" y2="6" />
|
||||||
|
<line x1="6" x2="6.01" y1="18" y2="18" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 692 B After Width: | Height: | Size: 413 B |
@ -1203,14 +1203,24 @@ impl Room {
|
|||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<u64>> {
|
) -> Task<Result<u64>> {
|
||||||
|
let request = if let Some(remote_project_id) = project.read(cx).remote_project_id() {
|
||||||
|
self.client.request(proto::ShareProject {
|
||||||
|
room_id: self.id(),
|
||||||
|
worktrees: vec![],
|
||||||
|
remote_project_id: Some(remote_project_id.0),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
if let Some(project_id) = project.read(cx).remote_id() {
|
if let Some(project_id) = project.read(cx).remote_id() {
|
||||||
return Task::ready(Ok(project_id));
|
return Task::ready(Ok(project_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = self.client.request(proto::ShareProject {
|
self.client.request(proto::ShareProject {
|
||||||
room_id: self.id(),
|
room_id: self.id(),
|
||||||
worktrees: project.read(cx).worktree_metadata_protos(cx),
|
worktrees: project.read(cx).worktree_metadata_protos(cx),
|
||||||
});
|
remote_project_id: None,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let response = request.await?;
|
let response = request.await?;
|
||||||
|
|
||||||
|
@ -11,9 +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::{
|
pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore};
|
||||||
Channel, ChannelEvent, ChannelMembership, ChannelStore, DevServer, RemoteProject,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod channel_store_tests;
|
mod channel_store_tests;
|
||||||
|
@ -3,10 +3,7 @@ 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::{
|
use client::{ChannelId, Client, ClientSettings, ProjectId, Subscription, User, UserId, UserStore};
|
||||||
ChannelId, Client, ClientSettings, DevServerId, ProjectId, RemoteProjectId, 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::{
|
||||||
@ -15,7 +12,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use language::Capability;
|
use language::Capability;
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{self, ChannelRole, ChannelVisibility, DevServerStatus},
|
proto::{self, ChannelRole, ChannelVisibility},
|
||||||
TypedEnvelope,
|
TypedEnvelope,
|
||||||
};
|
};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
@ -53,57 +50,12 @@ impl From<proto::HostedProject> for HostedProject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct RemoteProject {
|
|
||||||
pub id: RemoteProjectId,
|
|
||||||
pub project_id: Option<ProjectId>,
|
|
||||||
pub channel_id: ChannelId,
|
|
||||||
pub name: SharedString,
|
|
||||||
pub path: SharedString,
|
|
||||||
pub dev_server_id: DevServerId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<proto::RemoteProject> for RemoteProject {
|
|
||||||
fn from(project: proto::RemoteProject) -> Self {
|
|
||||||
Self {
|
|
||||||
id: RemoteProjectId(project.id),
|
|
||||||
project_id: project.project_id.map(|id| ProjectId(id)),
|
|
||||||
channel_id: ChannelId(project.channel_id),
|
|
||||||
name: project.name.into(),
|
|
||||||
path: project.path.into(),
|
|
||||||
dev_server_id: DevServerId(project.dev_server_id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DevServer {
|
|
||||||
pub id: DevServerId,
|
|
||||||
pub channel_id: ChannelId,
|
|
||||||
pub name: SharedString,
|
|
||||||
pub status: DevServerStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<proto::DevServer> for DevServer {
|
|
||||||
fn from(dev_server: proto::DevServer) -> Self {
|
|
||||||
Self {
|
|
||||||
id: DevServerId(dev_server.dev_server_id),
|
|
||||||
channel_id: ChannelId(dev_server.channel_id),
|
|
||||||
status: dev_server.status(),
|
|
||||||
name: dev_server.name.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ChannelStore {
|
pub struct ChannelStore {
|
||||||
pub channel_index: ChannelIndex,
|
pub channel_index: ChannelIndex,
|
||||||
channel_invitations: Vec<Arc<Channel>>,
|
channel_invitations: Vec<Arc<Channel>>,
|
||||||
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
||||||
channel_states: HashMap<ChannelId, ChannelState>,
|
channel_states: HashMap<ChannelId, ChannelState>,
|
||||||
hosted_projects: HashMap<ProjectId, HostedProject>,
|
hosted_projects: HashMap<ProjectId, HostedProject>,
|
||||||
remote_projects: HashMap<RemoteProjectId, RemoteProject>,
|
|
||||||
dev_servers: HashMap<DevServerId, DevServer>,
|
|
||||||
|
|
||||||
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
||||||
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
||||||
@ -133,8 +85,6 @@ pub struct ChannelState {
|
|||||||
observed_chat_message: Option<u64>,
|
observed_chat_message: Option<u64>,
|
||||||
role: Option<ChannelRole>,
|
role: Option<ChannelRole>,
|
||||||
projects: HashSet<ProjectId>,
|
projects: HashSet<ProjectId>,
|
||||||
dev_servers: HashSet<DevServerId>,
|
|
||||||
remote_projects: HashSet<RemoteProjectId>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
@ -265,8 +215,6 @@ impl ChannelStore {
|
|||||||
channel_index: ChannelIndex::default(),
|
channel_index: ChannelIndex::default(),
|
||||||
channel_participants: Default::default(),
|
channel_participants: Default::default(),
|
||||||
hosted_projects: Default::default(),
|
hosted_projects: Default::default(),
|
||||||
remote_projects: Default::default(),
|
|
||||||
dev_servers: Default::default(),
|
|
||||||
outgoing_invites: Default::default(),
|
outgoing_invites: Default::default(),
|
||||||
opened_buffers: Default::default(),
|
opened_buffers: Default::default(),
|
||||||
opened_chats: Default::default(),
|
opened_chats: Default::default(),
|
||||||
@ -366,40 +314,6 @@ impl ChannelStore {
|
|||||||
projects
|
projects
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dev_servers_for_id(&self, channel_id: ChannelId) -> Vec<DevServer> {
|
|
||||||
let mut dev_servers: Vec<DevServer> = self
|
|
||||||
.channel_states
|
|
||||||
.get(&channel_id)
|
|
||||||
.map(|state| state.dev_servers.clone())
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|id| self.dev_servers.get(&id).cloned())
|
|
||||||
.collect();
|
|
||||||
dev_servers.sort_by_key(|s| (s.name.clone(), s.id));
|
|
||||||
dev_servers
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_dev_server_by_id(&self, id: DevServerId) -> Option<&DevServer> {
|
|
||||||
self.dev_servers.get(&id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_remote_project_by_id(&self, id: RemoteProjectId) -> Option<&RemoteProject> {
|
|
||||||
self.remote_projects.get(&id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remote_projects_for_id(&self, channel_id: ChannelId) -> Vec<RemoteProject> {
|
|
||||||
let mut remote_projects: Vec<RemoteProject> = self
|
|
||||||
.channel_states
|
|
||||||
.get(&channel_id)
|
|
||||||
.map(|state| state.remote_projects.clone())
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|id| self.remote_projects.get(&id).cloned())
|
|
||||||
.collect();
|
|
||||||
remote_projects.sort_by_key(|p| (p.name.clone(), p.id));
|
|
||||||
remote_projects
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool {
|
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool {
|
||||||
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
|
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
|
||||||
if let OpenedModelHandle::Open(buffer) = buffer {
|
if let OpenedModelHandle::Open(buffer) = buffer {
|
||||||
@ -901,46 +815,6 @@ impl ChannelStore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_remote_project(
|
|
||||||
&mut self,
|
|
||||||
channel_id: ChannelId,
|
|
||||||
dev_server_id: DevServerId,
|
|
||||||
name: String,
|
|
||||||
path: String,
|
|
||||||
cx: &mut ModelContext<Self>,
|
|
||||||
) -> Task<Result<proto::CreateRemoteProjectResponse>> {
|
|
||||||
let client = self.client.clone();
|
|
||||||
cx.background_executor().spawn(async move {
|
|
||||||
client
|
|
||||||
.request(proto::CreateRemoteProject {
|
|
||||||
channel_id: channel_id.0,
|
|
||||||
dev_server_id: dev_server_id.0,
|
|
||||||
name,
|
|
||||||
path,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_dev_server(
|
|
||||||
&mut self,
|
|
||||||
channel_id: ChannelId,
|
|
||||||
name: String,
|
|
||||||
cx: &mut ModelContext<Self>,
|
|
||||||
) -> Task<Result<proto::CreateDevServerResponse>> {
|
|
||||||
let client = self.client.clone();
|
|
||||||
cx.background_executor().spawn(async move {
|
|
||||||
let result = client
|
|
||||||
.request(proto::CreateDevServer {
|
|
||||||
channel_id: channel_id.0,
|
|
||||||
name,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_channel_member_details(
|
pub fn get_channel_member_details(
|
||||||
&self,
|
&self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
@ -1221,11 +1095,7 @@ impl ChannelStore {
|
|||||||
|| !payload.latest_channel_message_ids.is_empty()
|
|| !payload.latest_channel_message_ids.is_empty()
|
||||||
|| !payload.latest_channel_buffer_versions.is_empty()
|
|| !payload.latest_channel_buffer_versions.is_empty()
|
||||||
|| !payload.hosted_projects.is_empty()
|
|| !payload.hosted_projects.is_empty()
|
||||||
|| !payload.deleted_hosted_projects.is_empty()
|
|| !payload.deleted_hosted_projects.is_empty();
|
||||||
|| !payload.dev_servers.is_empty()
|
|
||||||
|| !payload.deleted_dev_servers.is_empty()
|
|
||||||
|| !payload.remote_projects.is_empty()
|
|
||||||
|| !payload.deleted_remote_projects.is_empty();
|
|
||||||
|
|
||||||
if channels_changed {
|
if channels_changed {
|
||||||
if !payload.delete_channels.is_empty() {
|
if !payload.delete_channels.is_empty() {
|
||||||
@ -1313,60 +1183,6 @@ impl ChannelStore {
|
|||||||
.remove_hosted_project(old_project.project_id);
|
.remove_hosted_project(old_project.project_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for remote_project in payload.remote_projects {
|
|
||||||
let remote_project: RemoteProject = remote_project.into();
|
|
||||||
if let Some(old_remote_project) = self
|
|
||||||
.remote_projects
|
|
||||||
.insert(remote_project.id, remote_project.clone())
|
|
||||||
{
|
|
||||||
self.channel_states
|
|
||||||
.entry(old_remote_project.channel_id)
|
|
||||||
.or_default()
|
|
||||||
.remove_remote_project(old_remote_project.id);
|
|
||||||
}
|
|
||||||
self.channel_states
|
|
||||||
.entry(remote_project.channel_id)
|
|
||||||
.or_default()
|
|
||||||
.add_remote_project(remote_project.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
for remote_project_id in payload.deleted_remote_projects {
|
|
||||||
let remote_project_id = RemoteProjectId(remote_project_id);
|
|
||||||
|
|
||||||
if let Some(old_project) = self.remote_projects.remove(&remote_project_id) {
|
|
||||||
self.channel_states
|
|
||||||
.entry(old_project.channel_id)
|
|
||||||
.or_default()
|
|
||||||
.remove_remote_project(old_project.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for dev_server in payload.dev_servers {
|
|
||||||
let dev_server: DevServer = dev_server.into();
|
|
||||||
if let Some(old_server) = self.dev_servers.insert(dev_server.id, dev_server.clone())
|
|
||||||
{
|
|
||||||
self.channel_states
|
|
||||||
.entry(old_server.channel_id)
|
|
||||||
.or_default()
|
|
||||||
.remove_dev_server(old_server.id);
|
|
||||||
}
|
|
||||||
self.channel_states
|
|
||||||
.entry(dev_server.channel_id)
|
|
||||||
.or_default()
|
|
||||||
.add_dev_server(dev_server.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
for dev_server_id in payload.deleted_dev_servers {
|
|
||||||
let dev_server_id = DevServerId(dev_server_id);
|
|
||||||
|
|
||||||
if let Some(old_server) = self.dev_servers.remove(&dev_server_id) {
|
|
||||||
self.channel_states
|
|
||||||
.entry(old_server.channel_id)
|
|
||||||
.or_default()
|
|
||||||
.remove_dev_server(old_server.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@ -1481,20 +1297,4 @@ impl ChannelState {
|
|||||||
fn remove_hosted_project(&mut self, project_id: ProjectId) {
|
fn remove_hosted_project(&mut self, project_id: ProjectId) {
|
||||||
self.projects.remove(&project_id);
|
self.projects.remove(&project_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_remote_project(&mut self, remote_project_id: RemoteProjectId) {
|
|
||||||
self.remote_projects.insert(remote_project_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_remote_project(&mut self, remote_project_id: RemoteProjectId) {
|
|
||||||
self.remote_projects.remove(&remote_project_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_dev_server(&mut self, dev_server_id: DevServerId) {
|
|
||||||
self.dev_servers.insert(dev_server_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_dev_server(&mut self, dev_server_id: DevServerId) {
|
|
||||||
self.dev_servers.remove(&dev_server_id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,9 @@ pub struct ProjectId(pub u64);
|
|||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
pub struct DevServerId(pub u64);
|
pub struct DevServerId(pub u64);
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
#[derive(
|
||||||
|
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
|
||||||
|
)]
|
||||||
pub struct RemoteProjectId(pub u64);
|
pub struct RemoteProjectId(pub u64);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
@ -93,6 +93,7 @@ notifications = { workspace = true, features = ["test-support"] }
|
|||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
project = { workspace = true, features = ["test-support"] }
|
project = { workspace = true, features = ["test-support"] }
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
|
remote_projects.workspace = true
|
||||||
rpc = { workspace = true, features = ["test-support"] }
|
rpc = { workspace = true, features = ["test-support"] }
|
||||||
sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
|
sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
@ -398,26 +398,21 @@ CREATE TABLE hosted_projects (
|
|||||||
channel_id INTEGER NOT NULL REFERENCES channels(id),
|
channel_id INTEGER NOT NULL REFERENCES channels(id),
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
visibility TEXT NOT NULL,
|
visibility TEXT NOT NULL,
|
||||||
deleted_at TIMESTAMP NULL,
|
deleted_at TIMESTAMP NULL
|
||||||
dev_server_id INTEGER REFERENCES dev_servers(id),
|
|
||||||
dev_server_path TEXT
|
|
||||||
);
|
);
|
||||||
CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id);
|
CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id);
|
||||||
CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL);
|
CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL);
|
||||||
|
|
||||||
CREATE TABLE dev_servers (
|
CREATE TABLE dev_servers (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
channel_id INTEGER NOT NULL REFERENCES channels(id),
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
hashed_token TEXT NOT NULL
|
hashed_token TEXT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX idx_dev_servers_on_channel_id ON dev_servers (channel_id);
|
|
||||||
|
|
||||||
CREATE TABLE remote_projects (
|
CREATE TABLE remote_projects (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
channel_id INTEGER NOT NULL REFERENCES channels(id),
|
|
||||||
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
|
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
|
||||||
name TEXT NOT NULL,
|
|
||||||
path TEXT NOT NULL
|
path TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
DELETE FROM remote_projects;
|
||||||
|
DELETE FROM dev_servers;
|
||||||
|
|
||||||
|
ALTER TABLE dev_servers DROP COLUMN channel_id;
|
||||||
|
ALTER TABLE dev_servers ADD COLUMN user_id INT NOT NULL REFERENCES users(id);
|
||||||
|
|
||||||
|
ALTER TABLE remote_projects DROP COLUMN channel_id;
|
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE remote_projects DROP COLUMN name;
|
||||||
|
ALTER TABLE remote_projects
|
||||||
|
ADD CONSTRAINT unique_path_constraint UNIQUE(dev_server_id, path);
|
@ -655,8 +655,6 @@ pub struct ChannelsForUser {
|
|||||||
pub channel_memberships: Vec<channel_member::Model>,
|
pub channel_memberships: Vec<channel_member::Model>,
|
||||||
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
||||||
pub hosted_projects: Vec<proto::HostedProject>,
|
pub hosted_projects: Vec<proto::HostedProject>,
|
||||||
pub dev_servers: Vec<dev_server::Model>,
|
|
||||||
pub remote_projects: Vec<proto::RemoteProject>,
|
|
||||||
|
|
||||||
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
|
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
|
||||||
pub observed_channel_messages: Vec<proto::ChannelMessageId>,
|
pub observed_channel_messages: Vec<proto::ChannelMessageId>,
|
||||||
@ -764,6 +762,7 @@ pub struct Project {
|
|||||||
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>,
|
||||||
|
pub remote_project_id: Option<RemoteProjectId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ProjectCollaborator {
|
pub struct ProjectCollaborator {
|
||||||
@ -786,8 +785,7 @@ impl ProjectCollaborator {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct LeftProject {
|
pub struct LeftProject {
|
||||||
pub id: ProjectId,
|
pub id: ProjectId,
|
||||||
pub host_user_id: Option<UserId>,
|
pub should_unshare: bool,
|
||||||
pub host_connection_id: Option<ConnectionId>,
|
|
||||||
pub connection_ids: Vec<ConnectionId>,
|
pub connection_ids: Vec<ConnectionId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -640,15 +640,10 @@ impl Database {
|
|||||||
.get_hosted_projects(&channel_ids, &roles_by_channel_id, tx)
|
.get_hosted_projects(&channel_ids, &roles_by_channel_id, tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let dev_servers = self.get_dev_servers(&channel_ids, tx).await?;
|
|
||||||
let remote_projects = self.get_remote_projects(&channel_ids, tx).await?;
|
|
||||||
|
|
||||||
Ok(ChannelsForUser {
|
Ok(ChannelsForUser {
|
||||||
channel_memberships,
|
channel_memberships,
|
||||||
channels,
|
channels,
|
||||||
hosted_projects,
|
hosted_projects,
|
||||||
dev_servers,
|
|
||||||
remote_projects,
|
|
||||||
channel_participants,
|
channel_participants,
|
||||||
latest_buffer_versions,
|
latest_buffer_versions,
|
||||||
latest_channel_messages,
|
latest_channel_messages,
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
use sea_orm::{ActiveValue, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter};
|
use rpc::proto;
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveValue, ColumnTrait, DatabaseTransaction, EntityTrait, IntoActiveModel, QueryFilter,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{channel, dev_server, ChannelId, Database, DevServerId, UserId};
|
use super::{dev_server, remote_project, Database, DevServerId, UserId};
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub async fn get_dev_server(
|
pub async fn get_dev_server(
|
||||||
@ -16,40 +19,105 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_dev_servers(
|
pub async fn get_dev_servers(&self, user_id: UserId) -> crate::Result<Vec<dev_server::Model>> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
Ok(dev_server::Entity::find()
|
||||||
|
.filter(dev_server::Column::UserId.eq(user_id))
|
||||||
|
.all(&*tx)
|
||||||
|
.await?)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remote_projects_update(
|
||||||
&self,
|
&self,
|
||||||
channel_ids: &Vec<ChannelId>,
|
user_id: UserId,
|
||||||
|
) -> crate::Result<proto::RemoteProjectsUpdate> {
|
||||||
|
self.transaction(
|
||||||
|
|tx| async move { self.remote_projects_update_internal(user_id, &tx).await },
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remote_projects_update_internal(
|
||||||
|
&self,
|
||||||
|
user_id: UserId,
|
||||||
tx: &DatabaseTransaction,
|
tx: &DatabaseTransaction,
|
||||||
) -> crate::Result<Vec<dev_server::Model>> {
|
) -> crate::Result<proto::RemoteProjectsUpdate> {
|
||||||
let servers = dev_server::Entity::find()
|
let dev_servers = dev_server::Entity::find()
|
||||||
.filter(dev_server::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
|
.filter(dev_server::Column::UserId.eq(user_id))
|
||||||
.all(tx)
|
.all(tx)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(servers)
|
|
||||||
|
let remote_projects = remote_project::Entity::find()
|
||||||
|
.filter(
|
||||||
|
remote_project::Column::DevServerId
|
||||||
|
.is_in(dev_servers.iter().map(|d| d.id).collect::<Vec<_>>()),
|
||||||
|
)
|
||||||
|
.find_also_related(super::project::Entity)
|
||||||
|
.all(tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(proto::RemoteProjectsUpdate {
|
||||||
|
dev_servers: dev_servers
|
||||||
|
.into_iter()
|
||||||
|
.map(|d| d.to_proto(proto::DevServerStatus::Offline))
|
||||||
|
.collect(),
|
||||||
|
remote_projects: remote_projects
|
||||||
|
.into_iter()
|
||||||
|
.map(|(remote_project, project)| remote_project.to_proto(project))
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_dev_server(
|
pub async fn create_dev_server(
|
||||||
&self,
|
&self,
|
||||||
channel_id: ChannelId,
|
|
||||||
name: &str,
|
name: &str,
|
||||||
hashed_access_token: &str,
|
hashed_access_token: &str,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
) -> crate::Result<(channel::Model, dev_server::Model)> {
|
) -> crate::Result<(dev_server::Model, proto::RemoteProjectsUpdate)> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
|
||||||
self.check_user_is_channel_admin(&channel, user_id, &tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let dev_server = dev_server::Entity::insert(dev_server::ActiveModel {
|
let dev_server = dev_server::Entity::insert(dev_server::ActiveModel {
|
||||||
id: ActiveValue::NotSet,
|
id: ActiveValue::NotSet,
|
||||||
hashed_token: ActiveValue::Set(hashed_access_token.to_string()),
|
hashed_token: ActiveValue::Set(hashed_access_token.to_string()),
|
||||||
channel_id: ActiveValue::Set(channel_id),
|
|
||||||
name: ActiveValue::Set(name.to_string()),
|
name: ActiveValue::Set(name.to_string()),
|
||||||
|
user_id: ActiveValue::Set(user_id),
|
||||||
})
|
})
|
||||||
.exec_with_returning(&*tx)
|
.exec_with_returning(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok((channel, dev_server))
|
let remote_projects = self.remote_projects_update_internal(user_id, &tx).await?;
|
||||||
|
|
||||||
|
Ok((dev_server, remote_projects))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_dev_server(
|
||||||
|
&self,
|
||||||
|
id: DevServerId,
|
||||||
|
user_id: UserId,
|
||||||
|
) -> crate::Result<proto::RemoteProjectsUpdate> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else {
|
||||||
|
return Err(anyhow::anyhow!("no dev server with id {}", id))?;
|
||||||
|
};
|
||||||
|
if dev_server.user_id != user_id {
|
||||||
|
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
remote_project::Entity::delete_many()
|
||||||
|
.filter(remote_project::Column::DevServerId.eq(id))
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
dev_server::Entity::delete(dev_server.into_active_model())
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let remote_projects = self.remote_projects_update_internal(user_id, &tx).await?;
|
||||||
|
|
||||||
|
Ok(remote_projects)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ impl Database {
|
|||||||
room_id: RoomId,
|
room_id: RoomId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
worktrees: &[proto::WorktreeMetadata],
|
worktrees: &[proto::WorktreeMetadata],
|
||||||
|
remote_project_id: Option<RemoteProjectId>,
|
||||||
) -> Result<TransactionGuard<(ProjectId, proto::Room)>> {
|
) -> Result<TransactionGuard<(ProjectId, proto::Room)>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
let participant = room_participant::Entity::find()
|
let participant = room_participant::Entity::find()
|
||||||
@ -58,6 +59,30 @@ impl Database {
|
|||||||
return Err(anyhow!("guests cannot share projects"))?;
|
return Err(anyhow!("guests cannot share projects"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(remote_project_id) = remote_project_id {
|
||||||
|
let project = project::Entity::find()
|
||||||
|
.filter(project::Column::RemoteProjectId.eq(Some(remote_project_id)))
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no remote project"))?;
|
||||||
|
|
||||||
|
if project.room_id.is_some() {
|
||||||
|
return Err(anyhow!("project already shared"))?;
|
||||||
|
};
|
||||||
|
|
||||||
|
let project = project::Entity::update(project::ActiveModel {
|
||||||
|
room_id: ActiveValue::Set(Some(room_id)),
|
||||||
|
..project.into_active_model()
|
||||||
|
})
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// todo! check user is a project-collaborator
|
||||||
|
|
||||||
|
let room = self.get_room(room_id, &tx).await?;
|
||||||
|
return Ok((project.id, room));
|
||||||
|
}
|
||||||
|
|
||||||
let project = project::ActiveModel {
|
let project = project::ActiveModel {
|
||||||
room_id: ActiveValue::set(Some(participant.room_id)),
|
room_id: ActiveValue::set(Some(participant.room_id)),
|
||||||
host_user_id: ActiveValue::set(Some(participant.user_id)),
|
host_user_id: ActiveValue::set(Some(participant.user_id)),
|
||||||
@ -111,6 +136,7 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
|
user_id: Option<UserId>,
|
||||||
) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
|
) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
|
||||||
self.project_transaction(project_id, |tx| async move {
|
self.project_transaction(project_id, |tx| async move {
|
||||||
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?;
|
||||||
@ -118,19 +144,37 @@ impl Database {
|
|||||||
.one(&*tx)
|
.one(&*tx)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("project not found"))?;
|
.ok_or_else(|| anyhow!("project not found"))?;
|
||||||
if project.host_connection()? == connection {
|
|
||||||
let room = if let Some(room_id) = project.room_id {
|
let room = if let Some(room_id) = project.room_id {
|
||||||
Some(self.get_room(room_id, &tx).await?)
|
Some(self.get_room(room_id, &tx).await?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
if project.host_connection()? == connection {
|
||||||
project::Entity::delete(project.into_active_model())
|
project::Entity::delete(project.into_active_model())
|
||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
Ok((room, guest_connection_ids))
|
return Ok((room, guest_connection_ids));
|
||||||
} else {
|
}
|
||||||
|
if let Some(remote_project_id) = project.remote_project_id {
|
||||||
|
if let Some(user_id) = user_id {
|
||||||
|
if user_id
|
||||||
|
!= self
|
||||||
|
.owner_for_remote_project(remote_project_id, &tx)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
Err(anyhow!("cannot unshare a project hosted by another user"))?
|
Err(anyhow!("cannot unshare a project hosted by another user"))?
|
||||||
}
|
}
|
||||||
|
project::Entity::update(project::ActiveModel {
|
||||||
|
room_id: ActiveValue::Set(None),
|
||||||
|
..project.into_active_model()
|
||||||
|
})
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
return Ok((room, guest_connection_ids));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow!("cannot unshare a project hosted by another user"))?
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@ -753,6 +797,7 @@ impl Database {
|
|||||||
name: language_server.name,
|
name: language_server.name,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
remote_project_id: project.remote_project_id,
|
||||||
};
|
};
|
||||||
Ok((project, replica_id as ReplicaId))
|
Ok((project, replica_id as ReplicaId))
|
||||||
}
|
}
|
||||||
@ -794,8 +839,7 @@ impl Database {
|
|||||||
Ok(LeftProject {
|
Ok(LeftProject {
|
||||||
id: project.id,
|
id: project.id,
|
||||||
connection_ids,
|
connection_ids,
|
||||||
host_user_id: None,
|
should_unshare: false,
|
||||||
host_connection_id: None,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@ -832,7 +876,7 @@ impl Database {
|
|||||||
.find_related(project_collaborator::Entity)
|
.find_related(project_collaborator::Entity)
|
||||||
.all(&*tx)
|
.all(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
let connection_ids = collaborators
|
let connection_ids: Vec<ConnectionId> = collaborators
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|collaborator| collaborator.connection())
|
.map(|collaborator| collaborator.connection())
|
||||||
.collect();
|
.collect();
|
||||||
@ -870,8 +914,7 @@ impl Database {
|
|||||||
|
|
||||||
let left_project = LeftProject {
|
let left_project = LeftProject {
|
||||||
id: project_id,
|
id: project_id,
|
||||||
host_user_id: project.host_user_id,
|
should_unshare: connection == project.host_connection()?,
|
||||||
host_connection_id: Some(project.host_connection()?),
|
|
||||||
connection_ids,
|
connection_ids,
|
||||||
};
|
};
|
||||||
Ok((room, left_project))
|
Ok((room, left_project))
|
||||||
@ -914,7 +957,7 @@ impl Database {
|
|||||||
capability: Capability,
|
capability: Capability,
|
||||||
tx: &DatabaseTransaction,
|
tx: &DatabaseTransaction,
|
||||||
) -> Result<(project::Model, ChannelRole)> {
|
) -> Result<(project::Model, ChannelRole)> {
|
||||||
let (project, remote_project) = project::Entity::find_by_id(project_id)
|
let (mut project, remote_project) = project::Entity::find_by_id(project_id)
|
||||||
.find_also_related(remote_project::Entity)
|
.find_also_related(remote_project::Entity)
|
||||||
.one(tx)
|
.one(tx)
|
||||||
.await?
|
.await?
|
||||||
@ -933,27 +976,44 @@ impl Database {
|
|||||||
PrincipalId::UserId(user_id) => user_id,
|
PrincipalId::UserId(user_id) => user_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
let role = if let Some(remote_project) = remote_project {
|
let role_from_room = if let Some(room_id) = project.room_id {
|
||||||
let channel = channel::Entity::find_by_id(remote_project.channel_id)
|
room_participant::Entity::find()
|
||||||
.one(tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("no such channel"))?;
|
|
||||||
|
|
||||||
self.check_user_is_channel_participant(&channel, user_id, &tx)
|
|
||||||
.await?
|
|
||||||
} else if let Some(room_id) = project.room_id {
|
|
||||||
// what's the users role?
|
|
||||||
let current_participant = room_participant::Entity::find()
|
|
||||||
.filter(room_participant::Column::RoomId.eq(room_id))
|
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||||
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
|
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
|
||||||
.one(tx)
|
.one(tx)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("no such room"))?;
|
.and_then(|participant| participant.role)
|
||||||
|
|
||||||
current_participant.role.unwrap_or(ChannelRole::Guest)
|
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow!("not authorized to read projects"))?;
|
None
|
||||||
};
|
};
|
||||||
|
let role_from_remote_project = if let Some(remote_project) = remote_project {
|
||||||
|
let dev_server = dev_server::Entity::find_by_id(remote_project.dev_server_id)
|
||||||
|
.one(tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no such channel"))?;
|
||||||
|
if user_id == dev_server.user_id {
|
||||||
|
// If the user left the room "uncleanly" they may rejoin the
|
||||||
|
// remote project before leave_room runs. IN that case kick
|
||||||
|
// the project out of the room pre-emptively.
|
||||||
|
if role_from_room.is_none() {
|
||||||
|
project = project::Entity::update(project::ActiveModel {
|
||||||
|
room_id: ActiveValue::Set(None),
|
||||||
|
..project.into_active_model()
|
||||||
|
})
|
||||||
|
.exec(tx)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Some(ChannelRole::Admin)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let role = role_from_remote_project
|
||||||
|
.or(role_from_room)
|
||||||
|
.unwrap_or(ChannelRole::Banned);
|
||||||
|
|
||||||
match capability {
|
match capability {
|
||||||
Capability::ReadWrite => {
|
Capability::ReadWrite => {
|
||||||
|
@ -8,8 +8,8 @@ use sea_orm::{
|
|||||||
use crate::db::ProjectId;
|
use crate::db::ProjectId;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
channel, project, project_collaborator, remote_project, worktree, ChannelId, Database,
|
dev_server, project, project_collaborator, remote_project, worktree, Database, DevServerId,
|
||||||
DevServerId, RejoinedProject, RemoteProjectId, ResharedProject, ServerId, UserId,
|
RejoinedProject, RemoteProjectId, ResharedProject, ServerId, UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
@ -26,29 +26,6 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_remote_projects(
|
|
||||||
&self,
|
|
||||||
channel_ids: &Vec<ChannelId>,
|
|
||||||
tx: &DatabaseTransaction,
|
|
||||||
) -> crate::Result<Vec<proto::RemoteProject>> {
|
|
||||||
let servers = remote_project::Entity::find()
|
|
||||||
.filter(remote_project::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
|
|
||||||
.find_also_related(project::Entity)
|
|
||||||
.all(tx)
|
|
||||||
.await?;
|
|
||||||
Ok(servers
|
|
||||||
.into_iter()
|
|
||||||
.map(|(remote_project, project)| proto::RemoteProject {
|
|
||||||
id: remote_project.id.to_proto(),
|
|
||||||
project_id: project.map(|p| p.id.to_proto()),
|
|
||||||
channel_id: remote_project.channel_id.to_proto(),
|
|
||||||
name: remote_project.name,
|
|
||||||
dev_server_id: remote_project.dev_server_id.to_proto(),
|
|
||||||
path: remote_project.path,
|
|
||||||
})
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_remote_projects_for_dev_server(
|
pub async fn get_remote_projects_for_dev_server(
|
||||||
&self,
|
&self,
|
||||||
dev_server_id: DevServerId,
|
dev_server_id: DevServerId,
|
||||||
@ -64,8 +41,6 @@ impl Database {
|
|||||||
.map(|(remote_project, project)| proto::RemoteProject {
|
.map(|(remote_project, project)| proto::RemoteProject {
|
||||||
id: remote_project.id.to_proto(),
|
id: remote_project.id.to_proto(),
|
||||||
project_id: project.map(|p| p.id.to_proto()),
|
project_id: project.map(|p| p.id.to_proto()),
|
||||||
channel_id: remote_project.channel_id.to_proto(),
|
|
||||||
name: remote_project.name,
|
|
||||||
dev_server_id: remote_project.dev_server_id.to_proto(),
|
dev_server_id: remote_project.dev_server_id.to_proto(),
|
||||||
path: remote_project.path,
|
path: remote_project.path,
|
||||||
})
|
})
|
||||||
@ -74,6 +49,38 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn remote_project_ids_for_user(
|
||||||
|
&self,
|
||||||
|
user_id: UserId,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
) -> crate::Result<Vec<RemoteProjectId>> {
|
||||||
|
let dev_servers = dev_server::Entity::find()
|
||||||
|
.filter(dev_server::Column::UserId.eq(user_id))
|
||||||
|
.find_with_related(remote_project::Entity)
|
||||||
|
.all(tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(dev_servers
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|(_, projects)| projects.into_iter().map(|p| p.id))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn owner_for_remote_project(
|
||||||
|
&self,
|
||||||
|
remote_project_id: RemoteProjectId,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
) -> crate::Result<UserId> {
|
||||||
|
let dev_server = remote_project::Entity::find_by_id(remote_project_id)
|
||||||
|
.find_also_related(dev_server::Entity)
|
||||||
|
.one(tx)
|
||||||
|
.await?
|
||||||
|
.and_then(|(_, dev_server)| dev_server)
|
||||||
|
.ok_or_else(|| anyhow!("no remote project"))?;
|
||||||
|
|
||||||
|
Ok(dev_server.user_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_stale_dev_server_projects(
|
pub async fn get_stale_dev_server_projects(
|
||||||
&self,
|
&self,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
@ -95,28 +102,30 @@ impl Database {
|
|||||||
|
|
||||||
pub async fn create_remote_project(
|
pub async fn create_remote_project(
|
||||||
&self,
|
&self,
|
||||||
channel_id: ChannelId,
|
|
||||||
dev_server_id: DevServerId,
|
dev_server_id: DevServerId,
|
||||||
name: &str,
|
|
||||||
path: &str,
|
path: &str,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
) -> crate::Result<(channel::Model, remote_project::Model)> {
|
) -> crate::Result<(remote_project::Model, proto::RemoteProjectsUpdate)> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
let dev_server = dev_server::Entity::find_by_id(dev_server_id)
|
||||||
self.check_user_is_channel_admin(&channel, user_id, &tx)
|
.one(&*tx)
|
||||||
.await?;
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
|
||||||
|
if dev_server.user_id != user_id {
|
||||||
|
return Err(anyhow!("not your dev server"))?;
|
||||||
|
}
|
||||||
|
|
||||||
let project = remote_project::Entity::insert(remote_project::ActiveModel {
|
let project = remote_project::Entity::insert(remote_project::ActiveModel {
|
||||||
name: ActiveValue::Set(name.to_string()),
|
|
||||||
id: ActiveValue::NotSet,
|
id: ActiveValue::NotSet,
|
||||||
channel_id: ActiveValue::Set(channel_id),
|
|
||||||
dev_server_id: ActiveValue::Set(dev_server_id),
|
dev_server_id: ActiveValue::Set(dev_server_id),
|
||||||
path: ActiveValue::Set(path.to_string()),
|
path: ActiveValue::Set(path.to_string()),
|
||||||
})
|
})
|
||||||
.exec_with_returning(&*tx)
|
.exec_with_returning(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok((channel, project))
|
let status = self.remote_projects_update_internal(user_id, &tx).await?;
|
||||||
|
|
||||||
|
Ok((project, status))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@ -127,8 +136,13 @@ impl Database {
|
|||||||
dev_server_id: DevServerId,
|
dev_server_id: DevServerId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
worktrees: &[proto::WorktreeMetadata],
|
worktrees: &[proto::WorktreeMetadata],
|
||||||
) -> crate::Result<proto::RemoteProject> {
|
) -> crate::Result<(proto::RemoteProject, UserId, proto::RemoteProjectsUpdate)> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
|
let dev_server = dev_server::Entity::find_by_id(dev_server_id)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
|
||||||
|
|
||||||
let remote_project = remote_project::Entity::find_by_id(remote_project_id)
|
let remote_project = remote_project::Entity::find_by_id(remote_project_id)
|
||||||
.one(&*tx)
|
.one(&*tx)
|
||||||
.await?
|
.await?
|
||||||
@ -168,7 +182,15 @@ impl Database {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(remote_project.to_proto(Some(project)))
|
let status = self
|
||||||
|
.remote_projects_update_internal(dev_server.user_id, &tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
remote_project.to_proto(Some(project)),
|
||||||
|
dev_server.user_id,
|
||||||
|
status,
|
||||||
|
))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -849,11 +849,32 @@ impl Database {
|
|||||||
.into_values::<_, QueryProjectIds>()
|
.into_values::<_, QueryProjectIds>()
|
||||||
.all(&*tx)
|
.all(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// if any project in the room has a remote-project-id that belongs to a dev server that this user owns.
|
||||||
|
let remote_projects_for_user = self
|
||||||
|
.remote_project_ids_for_user(leaving_participant.user_id, &tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let remote_projects_to_unshare = project::Entity::find()
|
||||||
|
.filter(
|
||||||
|
Condition::all()
|
||||||
|
.add(project::Column::RoomId.eq(room_id))
|
||||||
|
.add(
|
||||||
|
project::Column::RemoteProjectId
|
||||||
|
.is_in(remote_projects_for_user.clone()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.all(&*tx)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|project| project.id)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
let mut left_projects = HashMap::default();
|
let mut left_projects = HashMap::default();
|
||||||
let mut collaborators = project_collaborator::Entity::find()
|
let mut collaborators = project_collaborator::Entity::find()
|
||||||
.filter(project_collaborator::Column::ProjectId.is_in(project_ids))
|
.filter(project_collaborator::Column::ProjectId.is_in(project_ids))
|
||||||
.stream(&*tx)
|
.stream(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
while let Some(collaborator) = collaborators.next().await {
|
while let Some(collaborator) = collaborators.next().await {
|
||||||
let collaborator = collaborator?;
|
let collaborator = collaborator?;
|
||||||
let left_project =
|
let left_project =
|
||||||
@ -861,9 +882,8 @@ impl Database {
|
|||||||
.entry(collaborator.project_id)
|
.entry(collaborator.project_id)
|
||||||
.or_insert(LeftProject {
|
.or_insert(LeftProject {
|
||||||
id: collaborator.project_id,
|
id: collaborator.project_id,
|
||||||
host_user_id: Default::default(),
|
|
||||||
connection_ids: Default::default(),
|
connection_ids: Default::default(),
|
||||||
host_connection_id: None,
|
should_unshare: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let collaborator_connection_id = collaborator.connection();
|
let collaborator_connection_id = collaborator.connection();
|
||||||
@ -871,9 +891,10 @@ impl Database {
|
|||||||
left_project.connection_ids.push(collaborator_connection_id);
|
left_project.connection_ids.push(collaborator_connection_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if collaborator.is_host {
|
if (collaborator.is_host && collaborator.connection() == connection)
|
||||||
left_project.host_user_id = Some(collaborator.user_id);
|
|| remote_projects_to_unshare.contains(&collaborator.project_id)
|
||||||
left_project.host_connection_id = Some(collaborator_connection_id);
|
{
|
||||||
|
left_project.should_unshare = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drop(collaborators);
|
drop(collaborators);
|
||||||
@ -915,6 +936,17 @@ impl Database {
|
|||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if !remote_projects_to_unshare.is_empty() {
|
||||||
|
project::Entity::update_many()
|
||||||
|
.filter(project::Column::Id.is_in(remote_projects_to_unshare))
|
||||||
|
.set(project::ActiveModel {
|
||||||
|
room_id: ActiveValue::Set(None),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
let (channel, room) = self.get_channel_room(room_id, &tx).await?;
|
let (channel, room) = self.get_channel_room(room_id, &tx).await?;
|
||||||
let deleted = if room.participants.is_empty() {
|
let deleted = if room.participants.is_empty() {
|
||||||
let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?;
|
let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?;
|
||||||
@ -1264,38 +1296,46 @@ impl Database {
|
|||||||
}
|
}
|
||||||
drop(db_participants);
|
drop(db_participants);
|
||||||
|
|
||||||
let mut db_projects = db_room
|
let db_projects = db_room
|
||||||
.find_related(project::Entity)
|
.find_related(project::Entity)
|
||||||
.find_with_related(worktree::Entity)
|
.find_with_related(worktree::Entity)
|
||||||
.stream(tx)
|
.all(tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
while let Some(row) = db_projects.next().await {
|
for (db_project, db_worktrees) in db_projects {
|
||||||
let (db_project, db_worktree) = row?;
|
|
||||||
let host_connection = db_project.host_connection()?;
|
let host_connection = db_project.host_connection()?;
|
||||||
if let Some(participant) = participants.get_mut(&host_connection) {
|
if let Some(participant) = participants.get_mut(&host_connection) {
|
||||||
let project = if let Some(project) = participant
|
|
||||||
.projects
|
|
||||||
.iter_mut()
|
|
||||||
.find(|project| project.id == db_project.id.to_proto())
|
|
||||||
{
|
|
||||||
project
|
|
||||||
} else {
|
|
||||||
participant.projects.push(proto::ParticipantProject {
|
participant.projects.push(proto::ParticipantProject {
|
||||||
id: db_project.id.to_proto(),
|
id: db_project.id.to_proto(),
|
||||||
worktree_root_names: Default::default(),
|
worktree_root_names: Default::default(),
|
||||||
});
|
});
|
||||||
participant.projects.last_mut().unwrap()
|
let project = participant.projects.last_mut().unwrap();
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(db_worktree) = db_worktree {
|
for db_worktree in db_worktrees {
|
||||||
|
if db_worktree.visible {
|
||||||
|
project.worktree_root_names.push(db_worktree.root_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(remote_project_id) = db_project.remote_project_id {
|
||||||
|
let host = self.owner_for_remote_project(remote_project_id, tx).await?;
|
||||||
|
if let Some((_, participant)) = participants
|
||||||
|
.iter_mut()
|
||||||
|
.find(|(_, v)| v.user_id == host.to_proto())
|
||||||
|
{
|
||||||
|
participant.projects.push(proto::ParticipantProject {
|
||||||
|
id: db_project.id.to_proto(),
|
||||||
|
worktree_root_names: Default::default(),
|
||||||
|
});
|
||||||
|
let project = participant.projects.last_mut().unwrap();
|
||||||
|
|
||||||
|
for db_worktree in db_worktrees {
|
||||||
if db_worktree.visible {
|
if db_worktree.visible {
|
||||||
project.worktree_root_names.push(db_worktree.root_name);
|
project.worktree_root_names.push(db_worktree.root_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drop(db_projects);
|
}
|
||||||
|
|
||||||
let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
|
let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
|
||||||
let mut followers = Vec::new();
|
let mut followers = Vec::new();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::db::{ChannelId, DevServerId};
|
use crate::db::{DevServerId, UserId};
|
||||||
use rpc::proto;
|
use rpc::proto;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
@ -8,20 +8,28 @@ pub struct Model {
|
|||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: DevServerId,
|
pub id: DevServerId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub channel_id: ChannelId,
|
pub user_id: UserId,
|
||||||
pub hashed_token: String,
|
pub hashed_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {}
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::remote_project::Entity")]
|
||||||
|
RemoteProject,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::remote_project::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::RemoteProject.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
pub fn to_proto(&self, status: proto::DevServerStatus) -> proto::DevServer {
|
pub fn to_proto(&self, status: proto::DevServerStatus) -> proto::DevServer {
|
||||||
proto::DevServer {
|
proto::DevServer {
|
||||||
dev_server_id: self.id.to_proto(),
|
dev_server_id: self.id.to_proto(),
|
||||||
channel_id: self.channel_id.to_proto(),
|
|
||||||
name: self.name.clone(),
|
name: self.name.clone(),
|
||||||
status: status as i32,
|
status: status as i32,
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use super::project;
|
use super::project;
|
||||||
use crate::db::{ChannelId, DevServerId, RemoteProjectId};
|
use crate::db::{DevServerId, RemoteProjectId};
|
||||||
use rpc::proto;
|
use rpc::proto;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
@ -8,9 +8,7 @@ use sea_orm::entity::prelude::*;
|
|||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: RemoteProjectId,
|
pub id: RemoteProjectId,
|
||||||
pub channel_id: ChannelId,
|
|
||||||
pub dev_server_id: DevServerId,
|
pub dev_server_id: DevServerId,
|
||||||
pub name: String,
|
|
||||||
pub path: String,
|
pub path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,6 +18,12 @@ impl ActiveModelBehavior for ActiveModel {}
|
|||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
#[sea_orm(has_one = "super::project::Entity")]
|
#[sea_orm(has_one = "super::project::Entity")]
|
||||||
Project,
|
Project,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::dev_server::Entity",
|
||||||
|
from = "Column::DevServerId",
|
||||||
|
to = "super::dev_server::Column::Id"
|
||||||
|
)]
|
||||||
|
DevServer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Related<super::project::Entity> for Entity {
|
impl Related<super::project::Entity> for Entity {
|
||||||
@ -28,14 +32,18 @@ impl Related<super::project::Entity> for Entity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Related<super::dev_server::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::DevServer.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
pub fn to_proto(&self, project: Option<project::Model>) -> proto::RemoteProject {
|
pub fn to_proto(&self, project: Option<project::Model>) -> proto::RemoteProject {
|
||||||
proto::RemoteProject {
|
proto::RemoteProject {
|
||||||
id: self.id.to_proto(),
|
id: self.id.to_proto(),
|
||||||
project_id: project.map(|p| p.id.to_proto()),
|
project_id: project.map(|p| p.id.to_proto()),
|
||||||
channel_id: self.channel_id.to_proto(),
|
|
||||||
dev_server_id: self.dev_server_id.to_proto(),
|
dev_server_id: self.dev_server_id.to_proto(),
|
||||||
name: self.name.clone(),
|
|
||||||
path: self.path.clone(),
|
path: self.path.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -535,18 +535,18 @@ async fn test_project_count(db: &Arc<Database>) {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
|
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
|
||||||
|
|
||||||
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
|
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
|
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
|
||||||
|
|
||||||
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
|
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
|
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
|
||||||
|
|
||||||
// Projects shared by admins aren't counted.
|
// Projects shared by admins aren't counted.
|
||||||
db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[])
|
db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[], None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
|
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
|
||||||
|
@ -255,6 +255,13 @@ impl DevServerSession {
|
|||||||
pub fn dev_server_id(&self) -> DevServerId {
|
pub fn dev_server_id(&self) -> DevServerId {
|
||||||
self.0.dev_server_id().unwrap()
|
self.0.dev_server_id().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dev_server(&self) -> &dev_server::Model {
|
||||||
|
match &self.0.principal {
|
||||||
|
Principal::DevServer(dev_server) => dev_server,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for DevServerSession {
|
impl Deref for DevServerSession {
|
||||||
@ -405,6 +412,7 @@ impl Server {
|
|||||||
.add_request_handler(user_handler(rejoin_remote_projects))
|
.add_request_handler(user_handler(rejoin_remote_projects))
|
||||||
.add_request_handler(user_handler(create_remote_project))
|
.add_request_handler(user_handler(create_remote_project))
|
||||||
.add_request_handler(user_handler(create_dev_server))
|
.add_request_handler(user_handler(create_dev_server))
|
||||||
|
.add_request_handler(user_handler(delete_dev_server))
|
||||||
.add_request_handler(dev_server_handler(share_remote_project))
|
.add_request_handler(dev_server_handler(share_remote_project))
|
||||||
.add_request_handler(dev_server_handler(shutdown_dev_server))
|
.add_request_handler(dev_server_handler(shutdown_dev_server))
|
||||||
.add_request_handler(dev_server_handler(reconnect_dev_server))
|
.add_request_handler(dev_server_handler(reconnect_dev_server))
|
||||||
@ -1044,10 +1052,12 @@ impl Server {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (contacts, channels_for_user, channel_invites) = future::try_join3(
|
let (contacts, channels_for_user, channel_invites, remote_projects) =
|
||||||
|
future::try_join4(
|
||||||
self.app_state.db.get_contacts(user.id),
|
self.app_state.db.get_contacts(user.id),
|
||||||
self.app_state.db.get_channels_for_user(user.id),
|
self.app_state.db.get_channels_for_user(user.id),
|
||||||
self.app_state.db.get_channel_invites_for_user(user.id),
|
self.app_state.db.get_channel_invites_for_user(user.id),
|
||||||
|
self.app_state.db.remote_projects_update(user.id),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -1067,9 +1077,10 @@ impl Server {
|
|||||||
)?;
|
)?;
|
||||||
self.peer.send(
|
self.peer.send(
|
||||||
connection_id,
|
connection_id,
|
||||||
build_channels_update(channels_for_user, channel_invites, &pool),
|
build_channels_update(channels_for_user, channel_invites),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
send_remote_projects_update(user.id, remote_projects, session).await;
|
||||||
|
|
||||||
if let Some(incoming_call) =
|
if let Some(incoming_call) =
|
||||||
self.app_state.db.incoming_call_for_user(user.id).await?
|
self.app_state.db.incoming_call_for_user(user.id).await?
|
||||||
@ -1087,9 +1098,6 @@ impl Server {
|
|||||||
};
|
};
|
||||||
pool.add_dev_server(connection_id, dev_server.id, zed_version);
|
pool.add_dev_server(connection_id, dev_server.id, zed_version);
|
||||||
}
|
}
|
||||||
update_dev_server_status(dev_server, proto::DevServerStatus::Online, &session)
|
|
||||||
.await;
|
|
||||||
// todo!() allow only one connection.
|
|
||||||
|
|
||||||
let projects = self
|
let projects = self
|
||||||
.app_state
|
.app_state
|
||||||
@ -1098,6 +1106,13 @@ impl Server {
|
|||||||
.await?;
|
.await?;
|
||||||
self.peer
|
self.peer
|
||||||
.send(connection_id, proto::DevServerInstructions { projects })?;
|
.send(connection_id, proto::DevServerInstructions { projects })?;
|
||||||
|
|
||||||
|
let status = self
|
||||||
|
.app_state
|
||||||
|
.db
|
||||||
|
.remote_projects_update(dev_server.user_id)
|
||||||
|
.await?;
|
||||||
|
send_remote_projects_update(dev_server.user_id, status, &session).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1401,10 +1416,8 @@ async fn connection_lost(
|
|||||||
|
|
||||||
update_user_contacts(session.user_id(), &session).await?;
|
update_user_contacts(session.user_id(), &session).await?;
|
||||||
},
|
},
|
||||||
Principal::DevServer(dev_server) => {
|
Principal::DevServer(_) => {
|
||||||
lost_dev_server_connection(&session).await?;
|
lost_dev_server_connection(&session.for_dev_server().unwrap()).await?;
|
||||||
update_dev_server_status(&dev_server, proto::DevServerStatus::Offline, &session)
|
|
||||||
.await;
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1941,6 +1954,9 @@ async fn share_project(
|
|||||||
RoomId::from_proto(request.room_id),
|
RoomId::from_proto(request.room_id),
|
||||||
session.connection_id,
|
session.connection_id,
|
||||||
&request.worktrees,
|
&request.worktrees,
|
||||||
|
request
|
||||||
|
.remote_project_id
|
||||||
|
.map(|id| RemoteProjectId::from_proto(id)),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
response.send(proto::ShareProjectResponse {
|
response.send(proto::ShareProjectResponse {
|
||||||
@ -1954,14 +1970,25 @@ async fn share_project(
|
|||||||
/// Unshare a project from the room.
|
/// Unshare a project from the room.
|
||||||
async fn unshare_project(message: proto::UnshareProject, session: Session) -> Result<()> {
|
async fn unshare_project(message: proto::UnshareProject, session: Session) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(message.project_id);
|
let project_id = ProjectId::from_proto(message.project_id);
|
||||||
unshare_project_internal(project_id, &session).await
|
unshare_project_internal(
|
||||||
|
project_id,
|
||||||
|
session.connection_id,
|
||||||
|
session.user_id(),
|
||||||
|
&session,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn unshare_project_internal(project_id: ProjectId, session: &Session) -> Result<()> {
|
async fn unshare_project_internal(
|
||||||
|
project_id: ProjectId,
|
||||||
|
connection_id: ConnectionId,
|
||||||
|
user_id: Option<UserId>,
|
||||||
|
session: &Session,
|
||||||
|
) -> Result<()> {
|
||||||
let (room, guest_connection_ids) = &*session
|
let (room, guest_connection_ids) = &*session
|
||||||
.db()
|
.db()
|
||||||
.await
|
.await
|
||||||
.unshare_project(project_id, session.connection_id)
|
.unshare_project(project_id, connection_id, user_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let message = proto::UnshareProject {
|
let message = proto::UnshareProject {
|
||||||
@ -1969,7 +1996,7 @@ async fn unshare_project_internal(project_id: ProjectId, session: &Session) -> R
|
|||||||
};
|
};
|
||||||
|
|
||||||
broadcast(
|
broadcast(
|
||||||
Some(session.connection_id),
|
Some(connection_id),
|
||||||
guest_connection_ids.iter().copied(),
|
guest_connection_ids.iter().copied(),
|
||||||
|conn_id| session.peer.send(conn_id, message.clone()),
|
|conn_id| session.peer.send(conn_id, message.clone()),
|
||||||
);
|
);
|
||||||
@ -1980,13 +2007,13 @@ async fn unshare_project_internal(project_id: ProjectId, session: &Session) -> R
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Share a project into the room.
|
/// DevServer makes a project available online
|
||||||
async fn share_remote_project(
|
async fn share_remote_project(
|
||||||
request: proto::ShareRemoteProject,
|
request: proto::ShareRemoteProject,
|
||||||
response: Response<proto::ShareRemoteProject>,
|
response: Response<proto::ShareRemoteProject>,
|
||||||
session: DevServerSession,
|
session: DevServerSession,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let remote_project = session
|
let (remote_project, user_id, status) = session
|
||||||
.db()
|
.db()
|
||||||
.await
|
.await
|
||||||
.share_remote_project(
|
.share_remote_project(
|
||||||
@ -2000,22 +2027,7 @@ async fn share_remote_project(
|
|||||||
return Err(anyhow!("failed to share remote project"))?;
|
return Err(anyhow!("failed to share remote project"))?;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (connection_id, _) in session
|
send_remote_projects_update(user_id, status, &session).await;
|
||||||
.connection_pool()
|
|
||||||
.await
|
|
||||||
.channel_connection_ids(ChannelId::from_proto(remote_project.channel_id))
|
|
||||||
{
|
|
||||||
session
|
|
||||||
.peer
|
|
||||||
.send(
|
|
||||||
connection_id,
|
|
||||||
proto::UpdateChannels {
|
|
||||||
remote_projects: vec![remote_project.clone()],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.trace_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
response.send(proto::ShareProjectResponse { project_id })?;
|
response.send(proto::ShareProjectResponse { project_id })?;
|
||||||
|
|
||||||
@ -2081,19 +2093,21 @@ fn join_project_internal(
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for collaborator in &collaborators {
|
let add_project_collaborator = proto::AddProjectCollaborator {
|
||||||
session
|
|
||||||
.peer
|
|
||||||
.send(
|
|
||||||
collaborator.peer_id.unwrap().into(),
|
|
||||||
proto::AddProjectCollaborator {
|
|
||||||
project_id: project_id.to_proto(),
|
project_id: project_id.to_proto(),
|
||||||
collaborator: Some(proto::Collaborator {
|
collaborator: Some(proto::Collaborator {
|
||||||
peer_id: Some(session.connection_id.into()),
|
peer_id: Some(session.connection_id.into()),
|
||||||
replica_id: replica_id.0 as u32,
|
replica_id: replica_id.0 as u32,
|
||||||
user_id: guest_user_id.to_proto(),
|
user_id: guest_user_id.to_proto(),
|
||||||
}),
|
}),
|
||||||
},
|
};
|
||||||
|
|
||||||
|
for collaborator in &collaborators {
|
||||||
|
session
|
||||||
|
.peer
|
||||||
|
.send(
|
||||||
|
collaborator.peer_id.unwrap().into(),
|
||||||
|
add_project_collaborator.clone(),
|
||||||
)
|
)
|
||||||
.trace_err();
|
.trace_err();
|
||||||
}
|
}
|
||||||
@ -2105,7 +2119,10 @@ fn join_project_internal(
|
|||||||
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
|
role: project.role.into(),
|
||||||
|
remote_project_id: project
|
||||||
|
.remote_project_id
|
||||||
|
.map(|remote_project_id| remote_project_id.0 as u64),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
for (worktree_id, worktree) in mem::take(&mut project.worktrees) {
|
for (worktree_id, worktree) in mem::take(&mut project.worktrees) {
|
||||||
@ -2188,8 +2205,6 @@ async fn leave_project(request: proto::LeaveProject, session: UserSession) -> Re
|
|||||||
let (room, project) = &*db.leave_project(project_id, sender_id).await?;
|
let (room, project) = &*db.leave_project(project_id, sender_id).await?;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
%project_id,
|
%project_id,
|
||||||
host_user_id = ?project.host_user_id,
|
|
||||||
host_connection_id = ?project.host_connection_id,
|
|
||||||
"leave project"
|
"leave project"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -2224,13 +2239,33 @@ async fn create_remote_project(
|
|||||||
response: Response<proto::CreateRemoteProject>,
|
response: Response<proto::CreateRemoteProject>,
|
||||||
session: UserSession,
|
session: UserSession,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (channel, remote_project) = session
|
let dev_server_id = DevServerId(request.dev_server_id as i32);
|
||||||
|
let dev_server_connection_id = session
|
||||||
|
.connection_pool()
|
||||||
|
.await
|
||||||
|
.dev_server_connection_id(dev_server_id);
|
||||||
|
let Some(dev_server_connection_id) = dev_server_connection_id else {
|
||||||
|
Err(ErrorCode::DevServerOffline
|
||||||
|
.message("Cannot create a remote project when the dev server is offline".to_string())
|
||||||
|
.anyhow())?
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = request.path.clone();
|
||||||
|
//Check that the path exists on the dev server
|
||||||
|
session
|
||||||
|
.peer
|
||||||
|
.forward_request(
|
||||||
|
session.connection_id,
|
||||||
|
dev_server_connection_id,
|
||||||
|
proto::ValidateRemoteProjectRequest { path: path.clone() },
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (remote_project, update) = session
|
||||||
.db()
|
.db()
|
||||||
.await
|
.await
|
||||||
.create_remote_project(
|
.create_remote_project(
|
||||||
ChannelId(request.channel_id as i32),
|
|
||||||
DevServerId(request.dev_server_id as i32),
|
DevServerId(request.dev_server_id as i32),
|
||||||
&request.name,
|
|
||||||
&request.path,
|
&request.path,
|
||||||
session.user_id(),
|
session.user_id(),
|
||||||
)
|
)
|
||||||
@ -2242,25 +2277,12 @@ async fn create_remote_project(
|
|||||||
.get_remote_projects_for_dev_server(remote_project.dev_server_id)
|
.get_remote_projects_for_dev_server(remote_project.dev_server_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let update = proto::UpdateChannels {
|
|
||||||
remote_projects: vec![remote_project.to_proto(None)],
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let connection_pool = session.connection_pool().await;
|
|
||||||
for (connection_id, role) in connection_pool.channel_connection_ids(channel.root_id()) {
|
|
||||||
if role.can_see_all_descendants() {
|
|
||||||
session.peer.send(connection_id, update.clone())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let dev_server_id = remote_project.dev_server_id;
|
|
||||||
let dev_server_connection_id = connection_pool.dev_server_connection_id(dev_server_id);
|
|
||||||
if let Some(dev_server_connection_id) = dev_server_connection_id {
|
|
||||||
session.peer.send(
|
session.peer.send(
|
||||||
dev_server_connection_id,
|
dev_server_connection_id,
|
||||||
proto::DevServerInstructions { projects },
|
proto::DevServerInstructions { projects },
|
||||||
)?;
|
)?;
|
||||||
}
|
|
||||||
|
send_remote_projects_update(session.user_id(), update, &session).await;
|
||||||
|
|
||||||
response.send(proto::CreateRemoteProjectResponse {
|
response.send(proto::CreateRemoteProjectResponse {
|
||||||
remote_project: Some(remote_project.to_proto(None)),
|
remote_project: Some(remote_project.to_proto(None)),
|
||||||
@ -2276,37 +2298,56 @@ async fn create_dev_server(
|
|||||||
let access_token = auth::random_token();
|
let access_token = auth::random_token();
|
||||||
let hashed_access_token = auth::hash_access_token(&access_token);
|
let hashed_access_token = auth::hash_access_token(&access_token);
|
||||||
|
|
||||||
let (channel, dev_server) = session
|
let (dev_server, status) = session
|
||||||
.db()
|
.db()
|
||||||
.await
|
.await
|
||||||
.create_dev_server(
|
.create_dev_server(&request.name, &hashed_access_token, session.user_id())
|
||||||
ChannelId(request.channel_id as i32),
|
|
||||||
&request.name,
|
|
||||||
&hashed_access_token,
|
|
||||||
session.user_id(),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let update = proto::UpdateChannels {
|
send_remote_projects_update(session.user_id(), status, &session).await;
|
||||||
dev_servers: vec![dev_server.to_proto(proto::DevServerStatus::Offline)],
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let connection_pool = session.connection_pool().await;
|
|
||||||
for (connection_id, role) in connection_pool.channel_connection_ids(channel.root_id()) {
|
|
||||||
if role.can_see_channel(channel.visibility) {
|
|
||||||
session.peer.send(connection_id, update.clone())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.send(proto::CreateDevServerResponse {
|
response.send(proto::CreateDevServerResponse {
|
||||||
dev_server_id: dev_server.id.0 as u64,
|
dev_server_id: dev_server.id.0 as u64,
|
||||||
channel_id: request.channel_id,
|
|
||||||
access_token: auth::generate_dev_server_token(dev_server.id.0 as usize, access_token),
|
access_token: auth::generate_dev_server_token(dev_server.id.0 as usize, access_token),
|
||||||
name: request.name.clone(),
|
name: request.name.clone(),
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn delete_dev_server(
|
||||||
|
request: proto::DeleteDevServer,
|
||||||
|
response: Response<proto::DeleteDevServer>,
|
||||||
|
session: UserSession,
|
||||||
|
) -> Result<()> {
|
||||||
|
let dev_server_id = DevServerId(request.dev_server_id as i32);
|
||||||
|
let dev_server = session.db().await.get_dev_server(dev_server_id).await?;
|
||||||
|
if dev_server.user_id != session.user_id() {
|
||||||
|
return Err(anyhow!(ErrorCode::Forbidden))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection_id = session
|
||||||
|
.connection_pool()
|
||||||
|
.await
|
||||||
|
.dev_server_connection_id(dev_server_id);
|
||||||
|
if let Some(connection_id) = connection_id {
|
||||||
|
shutdown_dev_server_internal(dev_server_id, connection_id, &session).await?;
|
||||||
|
session
|
||||||
|
.peer
|
||||||
|
.send(connection_id, proto::ShutdownDevServer {})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = session
|
||||||
|
.db()
|
||||||
|
.await
|
||||||
|
.delete_dev_server(dev_server_id, session.user_id())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
send_remote_projects_update(session.user_id(), status, &session).await;
|
||||||
|
|
||||||
|
response.send(proto::Ack {})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn rejoin_remote_projects(
|
async fn rejoin_remote_projects(
|
||||||
request: proto::RejoinRemoteProjects,
|
request: proto::RejoinRemoteProjects,
|
||||||
response: Response<proto::RejoinRemoteProjects>,
|
response: Response<proto::RejoinRemoteProjects>,
|
||||||
@ -2403,8 +2444,15 @@ async fn shutdown_dev_server(
|
|||||||
session: DevServerSession,
|
session: DevServerSession,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
response.send(proto::Ack {})?;
|
response.send(proto::Ack {})?;
|
||||||
|
shutdown_dev_server_internal(session.dev_server_id(), session.connection_id, &session).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn shutdown_dev_server_internal(
|
||||||
|
dev_server_id: DevServerId,
|
||||||
|
connection_id: ConnectionId,
|
||||||
|
session: &Session,
|
||||||
|
) -> Result<()> {
|
||||||
let (remote_projects, dev_server) = {
|
let (remote_projects, dev_server) = {
|
||||||
let dev_server_id = session.dev_server_id();
|
|
||||||
let db = session.db().await;
|
let db = session.db().await;
|
||||||
let remote_projects = db.get_remote_projects_for_dev_server(dev_server_id).await?;
|
let remote_projects = db.get_remote_projects_for_dev_server(dev_server_id).await?;
|
||||||
let dev_server = db.get_dev_server(dev_server_id).await?;
|
let dev_server = db.get_dev_server(dev_server_id).await?;
|
||||||
@ -2412,22 +2460,26 @@ async fn shutdown_dev_server(
|
|||||||
};
|
};
|
||||||
|
|
||||||
for project_id in remote_projects.iter().filter_map(|p| p.project_id) {
|
for project_id in remote_projects.iter().filter_map(|p| p.project_id) {
|
||||||
unshare_project_internal(ProjectId::from_proto(project_id), &session.0).await?;
|
unshare_project_internal(
|
||||||
|
ProjectId::from_proto(project_id),
|
||||||
|
connection_id,
|
||||||
|
None,
|
||||||
|
session,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let update = proto::UpdateChannels {
|
session
|
||||||
remote_projects,
|
|
||||||
dev_servers: vec![dev_server.to_proto(proto::DevServerStatus::Offline)],
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
for (connection_id, _) in session
|
|
||||||
.connection_pool()
|
.connection_pool()
|
||||||
.await
|
.await
|
||||||
.channel_connection_ids(dev_server.channel_id)
|
.set_dev_server_offline(dev_server_id);
|
||||||
{
|
|
||||||
session.peer.send(connection_id, update.clone()).trace_err();
|
let status = session
|
||||||
}
|
.db()
|
||||||
|
.await
|
||||||
|
.remote_projects_update(dev_server.user_id)
|
||||||
|
.await?;
|
||||||
|
send_remote_projects_update(dev_server.user_id, status, &session).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -4626,7 +4678,7 @@ fn notify_membership_updated(
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut update = build_channels_update(result.new_channels, vec![], connection_pool);
|
let mut update = build_channels_update(result.new_channels, vec![]);
|
||||||
update.delete_channels = result
|
update.delete_channels = result
|
||||||
.removed_channels
|
.removed_channels
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -4659,7 +4711,6 @@ fn build_update_user_channels(channels: &ChannelsForUser) -> proto::UpdateUserCh
|
|||||||
fn build_channels_update(
|
fn build_channels_update(
|
||||||
channels: ChannelsForUser,
|
channels: ChannelsForUser,
|
||||||
channel_invites: Vec<db::Channel>,
|
channel_invites: Vec<db::Channel>,
|
||||||
pool: &ConnectionPool,
|
|
||||||
) -> proto::UpdateChannels {
|
) -> proto::UpdateChannels {
|
||||||
let mut update = proto::UpdateChannels::default();
|
let mut update = proto::UpdateChannels::default();
|
||||||
|
|
||||||
@ -4684,13 +4735,6 @@ fn build_channels_update(
|
|||||||
}
|
}
|
||||||
|
|
||||||
update.hosted_projects = channels.hosted_projects;
|
update.hosted_projects = channels.hosted_projects;
|
||||||
update.dev_servers = channels
|
|
||||||
.dev_servers
|
|
||||||
.into_iter()
|
|
||||||
.map(|dev_server| dev_server.to_proto(pool.dev_server_status(dev_server.id)))
|
|
||||||
.collect();
|
|
||||||
update.remote_projects = channels.remote_projects;
|
|
||||||
|
|
||||||
update
|
update
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4777,24 +4821,19 @@ fn channel_updated(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_dev_server_status(
|
async fn send_remote_projects_update(
|
||||||
dev_server: &dev_server::Model,
|
user_id: UserId,
|
||||||
status: proto::DevServerStatus,
|
mut status: proto::RemoteProjectsUpdate,
|
||||||
session: &Session,
|
session: &Session,
|
||||||
) {
|
) {
|
||||||
let pool = session.connection_pool().await;
|
let pool = session.connection_pool().await;
|
||||||
let connections = pool.channel_connection_ids(dev_server.channel_id);
|
for dev_server in &mut status.dev_servers {
|
||||||
for (connection_id, _) in connections {
|
dev_server.status =
|
||||||
session
|
pool.dev_server_status(DevServerId(dev_server.dev_server_id as i32)) as i32;
|
||||||
.peer
|
}
|
||||||
.send(
|
let connections = pool.user_connection_ids(user_id);
|
||||||
connection_id,
|
for connection_id in connections {
|
||||||
proto::UpdateChannels {
|
session.peer.send(connection_id, status.clone()).trace_err();
|
||||||
dev_servers: vec![dev_server.to_proto(status)],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.trace_err();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4833,7 +4872,7 @@ async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()>
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn lost_dev_server_connection(session: &Session) -> Result<()> {
|
async fn lost_dev_server_connection(session: &DevServerSession) -> Result<()> {
|
||||||
log::info!("lost dev server connection, unsharing projects");
|
log::info!("lost dev server connection, unsharing projects");
|
||||||
let project_ids = session
|
let project_ids = session
|
||||||
.db()
|
.db()
|
||||||
@ -4843,9 +4882,14 @@ async fn lost_dev_server_connection(session: &Session) -> Result<()> {
|
|||||||
|
|
||||||
for project_id in project_ids {
|
for project_id in project_ids {
|
||||||
// not unshare re-checks the connection ids match, so we get away with no transaction
|
// not unshare re-checks the connection ids match, so we get away with no transaction
|
||||||
unshare_project_internal(project_id, &session).await?;
|
unshare_project_internal(project_id, session.connection_id, None, &session).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let user_id = session.dev_server().user_id;
|
||||||
|
let update = session.db().await.remote_projects_update(user_id).await?;
|
||||||
|
|
||||||
|
send_remote_projects_update(user_id, update, session).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4947,7 +4991,7 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
|
|||||||
|
|
||||||
fn project_left(project: &db::LeftProject, session: &UserSession) {
|
fn project_left(project: &db::LeftProject, session: &UserSession) {
|
||||||
for connection_id in &project.connection_ids {
|
for connection_id in &project.connection_ids {
|
||||||
if project.host_user_id == Some(session.user_id()) {
|
if project.should_unshare {
|
||||||
session
|
session
|
||||||
.peer
|
.peer
|
||||||
.send(
|
.send(
|
||||||
|
@ -13,6 +13,7 @@ pub struct ConnectionPool {
|
|||||||
connected_users: BTreeMap<UserId, ConnectedPrincipal>,
|
connected_users: BTreeMap<UserId, ConnectedPrincipal>,
|
||||||
connected_dev_servers: BTreeMap<DevServerId, ConnectionId>,
|
connected_dev_servers: BTreeMap<DevServerId, ConnectionId>,
|
||||||
channels: ChannelPool,
|
channels: ChannelPool,
|
||||||
|
offline_dev_servers: HashSet<DevServerId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize)]
|
#[derive(Default, Serialize)]
|
||||||
@ -106,12 +107,17 @@ impl ConnectionPool {
|
|||||||
}
|
}
|
||||||
PrincipalId::DevServerId(dev_server_id) => {
|
PrincipalId::DevServerId(dev_server_id) => {
|
||||||
self.connected_dev_servers.remove(&dev_server_id);
|
self.connected_dev_servers.remove(&dev_server_id);
|
||||||
|
self.offline_dev_servers.remove(&dev_server_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.connections.remove(&connection_id).unwrap();
|
self.connections.remove(&connection_id).unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_dev_server_offline(&mut self, dev_server_id: DevServerId) {
|
||||||
|
self.offline_dev_servers.insert(dev_server_id);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn connections(&self) -> impl Iterator<Item = &Connection> {
|
pub fn connections(&self) -> impl Iterator<Item = &Connection> {
|
||||||
self.connections.values()
|
self.connections.values()
|
||||||
}
|
}
|
||||||
@ -137,7 +143,9 @@ impl ConnectionPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn dev_server_status(&self, dev_server_id: DevServerId) -> proto::DevServerStatus {
|
pub fn dev_server_status(&self, dev_server_id: DevServerId) -> proto::DevServerStatus {
|
||||||
if self.dev_server_connection_id(dev_server_id).is_some() {
|
if self.dev_server_connection_id(dev_server_id).is_some()
|
||||||
|
&& !self.offline_dev_servers.contains(&dev_server_id)
|
||||||
|
{
|
||||||
proto::DevServerStatus::Online
|
proto::DevServerStatus::Online
|
||||||
} else {
|
} else {
|
||||||
proto::DevServerStatus::Offline
|
proto::DevServerStatus::Offline
|
||||||
|
@ -1023,6 +1023,8 @@ async fn test_channel_link_notifications(
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
// the new channel shows for b and c
|
// the new channel shows for b and c
|
||||||
assert_channels_list_shape(
|
assert_channels_list_shape(
|
||||||
client_a.channel_store(),
|
client_a.channel_store(),
|
||||||
|
@ -1,45 +1,40 @@
|
|||||||
use std::path::Path;
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
|
use call::ActiveCall;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::VisualTestContext;
|
use gpui::{TestAppContext, VisualTestContext, WindowHandle};
|
||||||
use rpc::proto::DevServerStatus;
|
use rpc::{proto::DevServerStatus, ErrorCode, ErrorExt};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use workspace::{AppState, Workspace};
|
||||||
|
|
||||||
use crate::tests::TestServer;
|
use crate::tests::{following_tests::join_channel, TestServer};
|
||||||
|
|
||||||
|
use super::TestClient;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
|
async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
|
||||||
let (server, client) = TestServer::start1(cx).await;
|
let (server, client) = TestServer::start1(cx).await;
|
||||||
|
|
||||||
let channel_id = server
|
let store = cx.update(|cx| remote_projects::Store::global(cx).clone());
|
||||||
.make_channel("test", None, (&client, cx), &mut [])
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let resp = client
|
let resp = store
|
||||||
.channel_store()
|
|
||||||
.update(cx, |store, cx| {
|
.update(cx, |store, cx| {
|
||||||
store.create_dev_server(channel_id, "server-1".to_string(), cx)
|
store.create_dev_server("server-1".to_string(), cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
client.channel_store().update(cx, |store, _| {
|
store.update(cx, |store, _| {
|
||||||
assert_eq!(store.dev_servers_for_id(channel_id).len(), 1);
|
assert_eq!(store.dev_servers().len(), 1);
|
||||||
assert_eq!(store.dev_servers_for_id(channel_id)[0].name, "server-1");
|
assert_eq!(store.dev_servers()[0].name, "server-1");
|
||||||
assert_eq!(
|
assert_eq!(store.dev_servers()[0].status, DevServerStatus::Offline);
|
||||||
store.dev_servers_for_id(channel_id)[0].status,
|
|
||||||
DevServerStatus::Offline
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let dev_server = server.create_dev_server(resp.access_token, cx2).await;
|
let dev_server = server.create_dev_server(resp.access_token, cx2).await;
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
client.channel_store().update(cx, |store, _| {
|
store.update(cx, |store, _| {
|
||||||
assert_eq!(
|
assert_eq!(store.dev_servers()[0].status, DevServerStatus::Online);
|
||||||
store.dev_servers_for_id(channel_id)[0].status,
|
|
||||||
DevServerStatus::Online
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
dev_server
|
dev_server
|
||||||
@ -54,13 +49,10 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
client
|
store
|
||||||
.channel_store()
|
|
||||||
.update(cx, |store, cx| {
|
.update(cx, |store, cx| {
|
||||||
store.create_remote_project(
|
store.create_remote_project(
|
||||||
channel_id,
|
|
||||||
client::DevServerId(resp.dev_server_id),
|
client::DevServerId(resp.dev_server_id),
|
||||||
"project-1".to_string(),
|
|
||||||
"/remote".to_string(),
|
"/remote".to_string(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@ -70,12 +62,11 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
|||||||
|
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
let remote_workspace = client
|
let remote_workspace = store
|
||||||
.channel_store()
|
|
||||||
.update(cx, |store, cx| {
|
.update(cx, |store, cx| {
|
||||||
let projects = store.remote_projects_for_id(channel_id);
|
let projects = store.remote_projects();
|
||||||
assert_eq!(projects.len(), 1);
|
assert_eq!(projects.len(), 1);
|
||||||
assert_eq!(projects[0].name, "project-1");
|
assert_eq!(projects[0].path, "/remote");
|
||||||
workspace::join_remote_project(
|
workspace::join_remote_project(
|
||||||
projects[0].project_id.unwrap(),
|
projects[0].project_id.unwrap(),
|
||||||
client.app_state.clone(),
|
client.app_state.clone(),
|
||||||
@ -87,19 +78,19 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
|||||||
|
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
let cx2 = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut();
|
let cx = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut();
|
||||||
cx2.simulate_keystrokes("cmd-p 1 enter");
|
cx.simulate_keystrokes("cmd-p 1 enter");
|
||||||
|
|
||||||
let editor = remote_workspace
|
let editor = remote_workspace
|
||||||
.update(cx2, |ws, cx| {
|
.update(cx, |ws, cx| {
|
||||||
ws.active_item_as::<Editor>(cx).unwrap().clone()
|
ws.active_item_as::<Editor>(cx).unwrap().clone()
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
editor.update(cx2, |ed, cx| {
|
editor.update(cx, |ed, cx| {
|
||||||
assert_eq!(ed.text(cx).to_string(), "remote\nremote\nremote");
|
assert_eq!(ed.text(cx).to_string(), "remote\nremote\nremote");
|
||||||
});
|
});
|
||||||
cx2.simulate_input("wow!");
|
cx.simulate_input("wow!");
|
||||||
cx2.simulate_keystrokes("cmd-s");
|
cx.simulate_keystrokes("cmd-s");
|
||||||
|
|
||||||
let content = dev_server
|
let content = dev_server
|
||||||
.fs()
|
.fs()
|
||||||
@ -108,3 +99,263 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(content, "wow!remote\nremote\nremote\n");
|
assert_eq!(content, "wow!remote\nremote\nremote\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_dev_server_env_files(
|
||||||
|
cx1: &mut gpui::TestAppContext,
|
||||||
|
cx2: &mut gpui::TestAppContext,
|
||||||
|
cx3: &mut gpui::TestAppContext,
|
||||||
|
) {
|
||||||
|
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
|
||||||
|
|
||||||
|
let (_dev_server, remote_workspace) =
|
||||||
|
create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
|
||||||
|
|
||||||
|
cx1.executor().run_until_parked();
|
||||||
|
|
||||||
|
let cx1 = VisualTestContext::from_window(remote_workspace.into(), cx1).as_mut();
|
||||||
|
cx1.simulate_keystrokes("cmd-p . e enter");
|
||||||
|
|
||||||
|
let editor = remote_workspace
|
||||||
|
.update(cx1, |ws, cx| {
|
||||||
|
ws.active_item_as::<Editor>(cx).unwrap().clone()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
editor.update(cx1, |ed, cx| {
|
||||||
|
assert_eq!(ed.text(cx).to_string(), "SECRET");
|
||||||
|
});
|
||||||
|
|
||||||
|
cx1.update(|cx| {
|
||||||
|
workspace::join_channel(
|
||||||
|
channel_id,
|
||||||
|
client1.app_state.clone(),
|
||||||
|
Some(remote_workspace),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx1.executor().run_until_parked();
|
||||||
|
|
||||||
|
remote_workspace
|
||||||
|
.update(cx1, |ws, cx| {
|
||||||
|
assert!(ws.project().read(cx).is_shared());
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
join_channel(channel_id, &client2, cx2).await.unwrap();
|
||||||
|
cx2.executor().run_until_parked();
|
||||||
|
|
||||||
|
let (workspace2, cx2) = client2.active_workspace(cx2);
|
||||||
|
let editor = workspace2.update(cx2, |ws, cx| {
|
||||||
|
ws.active_item_as::<Editor>(cx).unwrap().clone()
|
||||||
|
});
|
||||||
|
// TODO: it'd be nice to hide .env files from other people
|
||||||
|
editor.update(cx2, |ed, cx| {
|
||||||
|
assert_eq!(ed.text(cx).to_string(), "SECRET");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_remote_project(
|
||||||
|
server: &TestServer,
|
||||||
|
client_app_state: Arc<AppState>,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
cx_devserver: &mut TestAppContext,
|
||||||
|
) -> (TestClient, WindowHandle<Workspace>) {
|
||||||
|
let store = cx.update(|cx| remote_projects::Store::global(cx).clone());
|
||||||
|
|
||||||
|
let resp = store
|
||||||
|
.update(cx, |store, cx| {
|
||||||
|
store.create_dev_server("server-1".to_string(), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let dev_server = server
|
||||||
|
.create_dev_server(resp.access_token, cx_devserver)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
|
dev_server
|
||||||
|
.fs()
|
||||||
|
.insert_tree(
|
||||||
|
"/remote",
|
||||||
|
json!({
|
||||||
|
"1.txt": "remote\nremote\nremote",
|
||||||
|
".env": "SECRET",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
store
|
||||||
|
.update(cx, |store, cx| {
|
||||||
|
store.create_remote_project(
|
||||||
|
client::DevServerId(resp.dev_server_id),
|
||||||
|
"/remote".to_string(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
|
let workspace = store
|
||||||
|
.update(cx, |store, cx| {
|
||||||
|
let projects = store.remote_projects();
|
||||||
|
assert_eq!(projects.len(), 1);
|
||||||
|
assert_eq!(projects[0].path, "/remote");
|
||||||
|
workspace::join_remote_project(projects[0].project_id.unwrap(), client_app_state, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
|
(dev_server, workspace)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_dev_server_leave_room(
|
||||||
|
cx1: &mut gpui::TestAppContext,
|
||||||
|
cx2: &mut gpui::TestAppContext,
|
||||||
|
cx3: &mut gpui::TestAppContext,
|
||||||
|
) {
|
||||||
|
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
|
||||||
|
|
||||||
|
let (_dev_server, remote_workspace) =
|
||||||
|
create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
|
||||||
|
|
||||||
|
cx1.update(|cx| {
|
||||||
|
workspace::join_channel(
|
||||||
|
channel_id,
|
||||||
|
client1.app_state.clone(),
|
||||||
|
Some(remote_workspace),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx1.executor().run_until_parked();
|
||||||
|
|
||||||
|
remote_workspace
|
||||||
|
.update(cx1, |ws, cx| {
|
||||||
|
assert!(ws.project().read(cx).is_shared());
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
join_channel(channel_id, &client2, cx2).await.unwrap();
|
||||||
|
cx2.executor().run_until_parked();
|
||||||
|
|
||||||
|
cx1.update(|cx| ActiveCall::global(cx).update(cx, |active_call, cx| active_call.hang_up(cx)))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx1.executor().run_until_parked();
|
||||||
|
|
||||||
|
let (workspace, cx2) = client2.active_workspace(cx2);
|
||||||
|
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_dev_server_reconnect(
|
||||||
|
cx1: &mut gpui::TestAppContext,
|
||||||
|
cx2: &mut gpui::TestAppContext,
|
||||||
|
cx3: &mut gpui::TestAppContext,
|
||||||
|
) {
|
||||||
|
let (mut server, client1) = TestServer::start1(cx1).await;
|
||||||
|
let channel_id = server
|
||||||
|
.make_channel("test", None, (&client1, cx1), &mut [])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (_dev_server, remote_workspace) =
|
||||||
|
create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await;
|
||||||
|
|
||||||
|
cx1.update(|cx| {
|
||||||
|
workspace::join_channel(
|
||||||
|
channel_id,
|
||||||
|
client1.app_state.clone(),
|
||||||
|
Some(remote_workspace),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx1.executor().run_until_parked();
|
||||||
|
|
||||||
|
remote_workspace
|
||||||
|
.update(cx1, |ws, cx| {
|
||||||
|
assert!(ws.project().read(cx).is_shared());
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
drop(client1);
|
||||||
|
|
||||||
|
let client2 = server.create_client(cx2, "user_a").await;
|
||||||
|
|
||||||
|
let store = cx2.update(|cx| remote_projects::Store::global(cx).clone());
|
||||||
|
|
||||||
|
store
|
||||||
|
.update(cx2, |store, cx| {
|
||||||
|
let projects = store.remote_projects();
|
||||||
|
workspace::join_remote_project(
|
||||||
|
projects[0].project_id.unwrap(),
|
||||||
|
client2.app_state.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_create_remote_project_path_validation(
|
||||||
|
cx1: &mut gpui::TestAppContext,
|
||||||
|
cx2: &mut gpui::TestAppContext,
|
||||||
|
cx3: &mut gpui::TestAppContext,
|
||||||
|
) {
|
||||||
|
let (server, client1) = TestServer::start1(cx1).await;
|
||||||
|
let _channel_id = server
|
||||||
|
.make_channel("test", None, (&client1, cx1), &mut [])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Creating a project with a path that does exist should not fail
|
||||||
|
let (_dev_server, _) =
|
||||||
|
create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await;
|
||||||
|
|
||||||
|
cx1.executor().run_until_parked();
|
||||||
|
|
||||||
|
let store = cx1.update(|cx| remote_projects::Store::global(cx).clone());
|
||||||
|
|
||||||
|
let resp = store
|
||||||
|
.update(cx1, |store, cx| {
|
||||||
|
store.create_dev_server("server-2".to_string(), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx1.executor().run_until_parked();
|
||||||
|
|
||||||
|
let _dev_server = server.create_dev_server(resp.access_token, cx3).await;
|
||||||
|
|
||||||
|
cx1.executor().run_until_parked();
|
||||||
|
|
||||||
|
// Creating a remote project with a path that does not exist should fail
|
||||||
|
let result = store
|
||||||
|
.update(cx1, |store, cx| {
|
||||||
|
store.create_remote_project(
|
||||||
|
client::DevServerId(resp.dev_server_id),
|
||||||
|
"/notfound".to_string(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx1.executor().run_until_parked();
|
||||||
|
|
||||||
|
let error = result.unwrap_err();
|
||||||
|
assert!(matches!(
|
||||||
|
error.error_code(),
|
||||||
|
ErrorCode::RemoteProjectPathDoesNotExist
|
||||||
|
));
|
||||||
|
}
|
||||||
|
@ -3743,6 +3743,10 @@ async fn test_leaving_project(
|
|||||||
|
|
||||||
buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
|
buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
|
||||||
|
|
||||||
|
project_a.read_with(cx_a, |project, _| {
|
||||||
|
assert_eq!(project.collaborators().len(), 2);
|
||||||
|
});
|
||||||
|
|
||||||
// Drop client B's connection and ensure client A and client C observe client B leaving.
|
// Drop client B's connection and ensure client A and client C observe client B leaving.
|
||||||
client_b.disconnect(&cx_b.to_async());
|
client_b.disconnect(&cx_b.to_async());
|
||||||
executor.advance_clock(RECONNECT_TIMEOUT);
|
executor.advance_clock(RECONNECT_TIMEOUT);
|
||||||
|
@ -284,6 +284,7 @@ impl TestServer {
|
|||||||
collab_ui::init(&app_state, cx);
|
collab_ui::init(&app_state, cx);
|
||||||
file_finder::init(cx);
|
file_finder::init(cx);
|
||||||
menu::init();
|
menu::init();
|
||||||
|
remote_projects::init(client.clone(), cx);
|
||||||
settings::KeymapFile::load_asset("keymaps/default-macos.json", cx).unwrap();
|
settings::KeymapFile::load_asset("keymaps/default-macos.json", cx).unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -39,7 +39,6 @@ db.workspace = true
|
|||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
emojis.workspace = true
|
emojis.workspace = true
|
||||||
extensions_ui.workspace = true
|
extensions_ui.workspace = true
|
||||||
feature_flags.workspace = true
|
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
@ -305,10 +305,6 @@ impl ChannelView {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
ChannelBufferEvent::BufferEdited => {
|
ChannelBufferEvent::BufferEdited => {
|
||||||
// Emit the edited event on the editor context so that other views can update it's state (e.g. markdown preview)
|
|
||||||
self.editor.update(cx, |_, cx| {
|
|
||||||
cx.emit(EditorEvent::Edited);
|
|
||||||
});
|
|
||||||
if self.editor.read(cx).is_focused(cx) {
|
if self.editor.read(cx).is_focused(cx) {
|
||||||
self.acknowledge_buffer_version(cx);
|
self.acknowledge_buffer_version(cx);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
mod channel_modal;
|
mod channel_modal;
|
||||||
mod contact_finder;
|
mod contact_finder;
|
||||||
mod dev_server_modal;
|
|
||||||
|
|
||||||
use self::channel_modal::ChannelModal;
|
use self::channel_modal::ChannelModal;
|
||||||
use self::dev_server_modal::DevServerModal;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
|
channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
|
||||||
CollaborationPanelSettings,
|
CollaborationPanelSettings,
|
||||||
};
|
};
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use channel::{Channel, ChannelEvent, ChannelStore, RemoteProject};
|
use channel::{Channel, ChannelEvent, ChannelStore};
|
||||||
use client::{ChannelId, Client, Contact, ProjectId, User, UserStore};
|
use client::{ChannelId, Client, Contact, ProjectId, 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};
|
||||||
use feature_flags::{self, FeatureFlagAppExt};
|
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, anchored, canvas, deferred, div, fill, list, point, prelude::*, px, AnyElement,
|
actions, anchored, canvas, deferred, div, fill, list, point, prelude::*, px, AnyElement,
|
||||||
@ -27,7 +24,7 @@ use gpui::{
|
|||||||
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
|
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
|
||||||
use project::{Fs, Project};
|
use project::{Fs, Project};
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{self, ChannelVisibility, DevServerStatus, PeerId},
|
proto::{self, ChannelVisibility, PeerId},
|
||||||
ErrorCode, ErrorExt,
|
ErrorCode, ErrorExt,
|
||||||
};
|
};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
@ -191,7 +188,6 @@ enum ListEntry {
|
|||||||
id: ProjectId,
|
id: ProjectId,
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
},
|
},
|
||||||
RemoteProject(channel::RemoteProject),
|
|
||||||
Contact {
|
Contact {
|
||||||
contact: Arc<Contact>,
|
contact: Arc<Contact>,
|
||||||
calling: bool,
|
calling: bool,
|
||||||
@ -282,23 +278,10 @@ impl CollabPanel {
|
|||||||
.push(cx.observe(&this.user_store, |this, _, cx| {
|
.push(cx.observe(&this.user_store, |this, _, cx| {
|
||||||
this.update_entries(true, cx)
|
this.update_entries(true, cx)
|
||||||
}));
|
}));
|
||||||
let mut has_opened = false;
|
this.subscriptions
|
||||||
this.subscriptions.push(cx.observe(
|
.push(cx.observe(&this.channel_store, move |this, _, cx| {
|
||||||
&this.channel_store,
|
|
||||||
move |this, channel_store, cx| {
|
|
||||||
if !has_opened {
|
|
||||||
if !channel_store
|
|
||||||
.read(cx)
|
|
||||||
.dev_servers_for_id(ChannelId(1))
|
|
||||||
.is_empty()
|
|
||||||
{
|
|
||||||
this.manage_remote_projects(ChannelId(1), cx);
|
|
||||||
has_opened = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.update_entries(true, cx)
|
this.update_entries(true, cx)
|
||||||
},
|
}));
|
||||||
));
|
|
||||||
this.subscriptions
|
this.subscriptions
|
||||||
.push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
|
.push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
|
||||||
this.subscriptions.push(cx.subscribe(
|
this.subscriptions.push(cx.subscribe(
|
||||||
@ -586,7 +569,6 @@ impl CollabPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let hosted_projects = channel_store.projects_for_id(channel.id);
|
let hosted_projects = channel_store.projects_for_id(channel.id);
|
||||||
let remote_projects = channel_store.remote_projects_for_id(channel.id);
|
|
||||||
let has_children = channel_store
|
let has_children = channel_store
|
||||||
.channel_at_index(mat.candidate_id + 1)
|
.channel_at_index(mat.candidate_id + 1)
|
||||||
.map_or(false, |next_channel| {
|
.map_or(false, |next_channel| {
|
||||||
@ -624,12 +606,6 @@ impl CollabPanel {
|
|||||||
for (name, id) in hosted_projects {
|
for (name, id) in hosted_projects {
|
||||||
self.entries.push(ListEntry::HostedProject { id, name });
|
self.entries.push(ListEntry::HostedProject { id, name });
|
||||||
}
|
}
|
||||||
|
|
||||||
if cx.has_flag::<feature_flags::Remoting>() {
|
|
||||||
for remote_project in remote_projects {
|
|
||||||
self.entries.push(ListEntry::RemoteProject(remote_project));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1089,59 +1065,6 @@ impl CollabPanel {
|
|||||||
.tooltip(move |cx| Tooltip::text("Open Project", cx))
|
.tooltip(move |cx| Tooltip::text("Open Project", cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_remote_project(
|
|
||||||
&self,
|
|
||||||
remote_project: &RemoteProject,
|
|
||||||
is_selected: bool,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let id = remote_project.id;
|
|
||||||
let name = remote_project.name.clone();
|
|
||||||
let maybe_project_id = remote_project.project_id;
|
|
||||||
|
|
||||||
let dev_server = self
|
|
||||||
.channel_store
|
|
||||||
.read(cx)
|
|
||||||
.find_dev_server_by_id(remote_project.dev_server_id);
|
|
||||||
|
|
||||||
let tooltip_text = SharedString::from(match dev_server {
|
|
||||||
Some(dev_server) => format!("Open Remote Project ({})", dev_server.name),
|
|
||||||
None => "Open Remote Project".to_string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let dev_server_is_online = dev_server.map(|s| s.status) == Some(DevServerStatus::Online);
|
|
||||||
|
|
||||||
let dev_server_text_color = if dev_server_is_online {
|
|
||||||
Color::Default
|
|
||||||
} else {
|
|
||||||
Color::Disabled
|
|
||||||
};
|
|
||||||
|
|
||||||
ListItem::new(ElementId::NamedInteger(
|
|
||||||
"remote-project".into(),
|
|
||||||
id.0 as usize,
|
|
||||||
))
|
|
||||||
.indent_level(2)
|
|
||||||
.indent_step_size(px(20.))
|
|
||||||
.selected(is_selected)
|
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
|
||||||
//TODO display error message if dev server is offline
|
|
||||||
if dev_server_is_online {
|
|
||||||
if let Some(project_id) = maybe_project_id {
|
|
||||||
this.join_remote_project(project_id, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.start_slot(
|
|
||||||
h_flex()
|
|
||||||
.relative()
|
|
||||||
.gap_1()
|
|
||||||
.child(IconButton::new(0, IconName::FileTree).icon_color(dev_server_text_color)),
|
|
||||||
)
|
|
||||||
.child(Label::new(name.clone()).color(dev_server_text_color))
|
|
||||||
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), 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 {
|
||||||
@ -1343,24 +1266,11 @@ impl CollabPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.channel_store.read(cx).is_root_channel(channel_id) {
|
if self.channel_store.read(cx).is_root_channel(channel_id) {
|
||||||
context_menu = context_menu
|
context_menu = context_menu.separator().entry(
|
||||||
.separator()
|
|
||||||
.entry(
|
|
||||||
"Manage Members",
|
"Manage Members",
|
||||||
None,
|
None,
|
||||||
cx.handler_for(&this, move |this, cx| {
|
cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
|
||||||
this.manage_members(channel_id, cx)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
.when(cx.has_flag::<feature_flags::Remoting>(), |context_menu| {
|
|
||||||
context_menu.entry(
|
|
||||||
"Manage Remote Projects",
|
|
||||||
None,
|
|
||||||
cx.handler_for(&this, move |this, cx| {
|
|
||||||
this.manage_remote_projects(channel_id, cx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
context_menu = context_menu.entry(
|
context_menu = context_menu.entry(
|
||||||
"Move this channel",
|
"Move this channel",
|
||||||
@ -1624,12 +1534,6 @@ impl CollabPanel {
|
|||||||
} => {
|
} => {
|
||||||
// todo()
|
// todo()
|
||||||
}
|
}
|
||||||
ListEntry::RemoteProject(project) => {
|
|
||||||
if let Some(project_id) = project.project_id {
|
|
||||||
self.join_remote_project(project_id, cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ListEntry::OutgoingRequest(_) => {}
|
ListEntry::OutgoingRequest(_) => {}
|
||||||
ListEntry::ChannelEditor { .. } => {}
|
ListEntry::ChannelEditor { .. } => {}
|
||||||
}
|
}
|
||||||
@ -1801,18 +1705,6 @@ impl CollabPanel {
|
|||||||
self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
|
self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manage_remote_projects(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
|
||||||
let channel_store = self.channel_store.clone();
|
|
||||||
let Some(workspace) = self.workspace.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
workspace.update(cx, |workspace, cx| {
|
|
||||||
workspace.toggle_modal(cx, |cx| {
|
|
||||||
DevServerModal::new(channel_store.clone(), channel_id, cx)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
|
fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(channel) = self.selected_channel() {
|
if let Some(channel) = self.selected_channel() {
|
||||||
self.remove_channel(channel.id, cx)
|
self.remove_channel(channel.id, cx)
|
||||||
@ -2113,18 +2005,6 @@ impl CollabPanel {
|
|||||||
.detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
|
.detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn join_remote_project(&mut self, project_id: ProjectId, cx: &mut ViewContext<Self>) {
|
|
||||||
let Some(workspace) = self.workspace.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let app_state = workspace.read(cx).app_state().clone();
|
|
||||||
workspace::join_remote_project(project_id, app_state, cx).detach_and_prompt_err(
|
|
||||||
"Failed to join project",
|
|
||||||
cx,
|
|
||||||
|_, _| None,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||||
let Some(workspace) = self.workspace.upgrade() else {
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
return;
|
return;
|
||||||
@ -2260,9 +2140,6 @@ impl CollabPanel {
|
|||||||
ListEntry::HostedProject { id, name } => self
|
ListEntry::HostedProject { id, name } => self
|
||||||
.render_channel_project(*id, name, is_selected, cx)
|
.render_channel_project(*id, name, is_selected, cx)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
ListEntry::RemoteProject(remote_project) => self
|
|
||||||
.render_remote_project(remote_project, is_selected, cx)
|
|
||||||
.into_any_element(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3005,11 +2882,6 @@ impl PartialEq for ListEntry {
|
|||||||
return id == other_id;
|
return id == other_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ListEntry::RemoteProject(project) => {
|
|
||||||
if let ListEntry::RemoteProject(other) = other {
|
|
||||||
return project.id == other.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListEntry::ChannelNotes { channel_id } => {
|
ListEntry::ChannelNotes { channel_id } => {
|
||||||
if let ListEntry::ChannelNotes {
|
if let ListEntry::ChannelNotes {
|
||||||
channel_id: other_id,
|
channel_id: other_id,
|
||||||
|
@ -1,622 +0,0 @@
|
|||||||
use channel::{ChannelStore, DevServer, RemoteProject};
|
|
||||||
use client::{ChannelId, DevServerId, RemoteProjectId};
|
|
||||||
use editor::Editor;
|
|
||||||
use gpui::{
|
|
||||||
AppContext, ClipboardItem, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
|
|
||||||
ScrollHandle, Task, View, ViewContext,
|
|
||||||
};
|
|
||||||
use rpc::proto::{self, CreateDevServerResponse, DevServerStatus};
|
|
||||||
use ui::{prelude::*, Indicator, List, ListHeader, ModalContent, ModalHeader, Tooltip};
|
|
||||||
use util::ResultExt;
|
|
||||||
use workspace::ModalView;
|
|
||||||
|
|
||||||
pub struct DevServerModal {
|
|
||||||
mode: Mode,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
scroll_handle: ScrollHandle,
|
|
||||||
channel_store: Model<ChannelStore>,
|
|
||||||
channel_id: ChannelId,
|
|
||||||
remote_project_name_editor: View<Editor>,
|
|
||||||
remote_project_path_editor: View<Editor>,
|
|
||||||
dev_server_name_editor: View<Editor>,
|
|
||||||
_subscriptions: [gpui::Subscription; 2],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct CreateDevServer {
|
|
||||||
creating: Option<Task<()>>,
|
|
||||||
dev_server: Option<CreateDevServerResponse>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CreateRemoteProject {
|
|
||||||
dev_server_id: DevServerId,
|
|
||||||
creating: Option<Task<()>>,
|
|
||||||
remote_project: Option<proto::RemoteProject>,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Mode {
|
|
||||||
Default,
|
|
||||||
CreateRemoteProject(CreateRemoteProject),
|
|
||||||
CreateDevServer(CreateDevServer),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DevServerModal {
|
|
||||||
pub fn new(
|
|
||||||
channel_store: Model<ChannelStore>,
|
|
||||||
channel_id: ChannelId,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let name_editor = cx.new_view(|cx| Editor::single_line(cx));
|
|
||||||
let path_editor = cx.new_view(|cx| Editor::single_line(cx));
|
|
||||||
let dev_server_name_editor = cx.new_view(|cx| {
|
|
||||||
let mut editor = Editor::single_line(cx);
|
|
||||||
editor.set_placeholder_text("Dev server name", cx);
|
|
||||||
editor
|
|
||||||
});
|
|
||||||
|
|
||||||
let focus_handle = cx.focus_handle();
|
|
||||||
|
|
||||||
let subscriptions = [
|
|
||||||
cx.observe(&channel_store, |_, _, cx| {
|
|
||||||
cx.notify();
|
|
||||||
}),
|
|
||||||
cx.on_focus_out(&focus_handle, |_, _cx| { /* cx.emit(DismissEvent) */ }),
|
|
||||||
];
|
|
||||||
|
|
||||||
Self {
|
|
||||||
mode: Mode::Default,
|
|
||||||
focus_handle,
|
|
||||||
scroll_handle: ScrollHandle::new(),
|
|
||||||
channel_store,
|
|
||||||
channel_id,
|
|
||||||
remote_project_name_editor: name_editor,
|
|
||||||
remote_project_path_editor: path_editor,
|
|
||||||
dev_server_name_editor,
|
|
||||||
_subscriptions: subscriptions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_remote_project(
|
|
||||||
&mut self,
|
|
||||||
dev_server_id: DevServerId,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
let channel_id = self.channel_id;
|
|
||||||
let name = self
|
|
||||||
.remote_project_name_editor
|
|
||||||
.read(cx)
|
|
||||||
.text(cx)
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
let path = self
|
|
||||||
.remote_project_path_editor
|
|
||||||
.read(cx)
|
|
||||||
.text(cx)
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
if name == "" {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if path == "" {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let create = self.channel_store.update(cx, |store, cx| {
|
|
||||||
store.create_remote_project(channel_id, dev_server_id, name, path, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let task = cx.spawn(|this, mut cx| async move {
|
|
||||||
let result = create.await;
|
|
||||||
if let Err(e) = &result {
|
|
||||||
cx.prompt(
|
|
||||||
gpui::PromptLevel::Critical,
|
|
||||||
"Failed to create project",
|
|
||||||
Some(&format!("{:?}. Please try again.", e)),
|
|
||||||
&["Ok"],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
this.update(&mut cx, |this, _| {
|
|
||||||
this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
|
|
||||||
dev_server_id,
|
|
||||||
creating: None,
|
|
||||||
remote_project: result.ok().and_then(|r| r.remote_project),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.mode = Mode::CreateRemoteProject(CreateRemoteProject {
|
|
||||||
dev_server_id,
|
|
||||||
creating: Some(task),
|
|
||||||
remote_project: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_dev_server(&mut self, cx: &mut ViewContext<Self>) {
|
|
||||||
let name = self
|
|
||||||
.dev_server_name_editor
|
|
||||||
.read(cx)
|
|
||||||
.text(cx)
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
if name == "" {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dev_server = self.channel_store.update(cx, |store, cx| {
|
|
||||||
store.create_dev_server(self.channel_id, name.clone(), cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let task = cx.spawn(|this, mut cx| async move {
|
|
||||||
match dev_server.await {
|
|
||||||
Ok(dev_server) => {
|
|
||||||
this.update(&mut cx, |this, _| {
|
|
||||||
this.mode = Mode::CreateDevServer(CreateDevServer {
|
|
||||||
creating: None,
|
|
||||||
dev_server: Some(dev_server),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
cx.prompt(
|
|
||||||
gpui::PromptLevel::Critical,
|
|
||||||
"Failed to create server",
|
|
||||||
Some(&format!("{:?}. Please try again.", e)),
|
|
||||||
&["Ok"],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.log_err();
|
|
||||||
this.update(&mut cx, |this, _| {
|
|
||||||
this.mode = Mode::CreateDevServer(Default::default());
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.mode = Mode::CreateDevServer(CreateDevServer {
|
|
||||||
creating: Some(task),
|
|
||||||
dev_server: None,
|
|
||||||
});
|
|
||||||
cx.notify()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
|
||||||
match self.mode {
|
|
||||||
Mode::Default => cx.emit(DismissEvent),
|
|
||||||
Mode::CreateRemoteProject(_) | Mode::CreateDevServer(_) => {
|
|
||||||
self.mode = Mode::Default;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_dev_server(
|
|
||||||
&mut self,
|
|
||||||
dev_server: &DevServer,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let channel_store = self.channel_store.read(cx);
|
|
||||||
let dev_server_id = dev_server.id;
|
|
||||||
let status = dev_server.status;
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.group("dev-server")
|
|
||||||
.justify_between()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.id(("status", dev_server.id.0))
|
|
||||||
.relative()
|
|
||||||
.child(Icon::new(IconName::Server).size(IconSize::Small))
|
|
||||||
.child(
|
|
||||||
div().absolute().bottom_0().left(rems_from_px(8.0)).child(
|
|
||||||
Indicator::dot().color(match status {
|
|
||||||
DevServerStatus::Online => Color::Created,
|
|
||||||
DevServerStatus::Offline => Color::Deleted,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.tooltip(move |cx| {
|
|
||||||
Tooltip::text(
|
|
||||||
match status {
|
|
||||||
DevServerStatus::Online => "Online",
|
|
||||||
DevServerStatus::Offline => "Offline",
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(dev_server.name.clone())
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.visible_on_hover("dev-server")
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
IconButton::new("edit-dev-server", IconName::Pencil)
|
|
||||||
.disabled(true) //TODO implement this on the collab side
|
|
||||||
.tooltip(|cx| {
|
|
||||||
Tooltip::text("Coming Soon - Edit dev server", cx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
IconButton::new("remove-dev-server", IconName::Trash)
|
|
||||||
.disabled(true) //TODO implement this on the collab side
|
|
||||||
.tooltip(|cx| {
|
|
||||||
Tooltip::text("Coming Soon - Remove dev server", cx)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex().gap_1().child(
|
|
||||||
IconButton::new("add-remote-project", IconName::Plus)
|
|
||||||
.tooltip(|cx| Tooltip::text("Add a remote project", cx))
|
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
|
||||||
this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
|
|
||||||
dev_server_id,
|
|
||||||
creating: None,
|
|
||||||
remote_project: None,
|
|
||||||
});
|
|
||||||
cx.notify();
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.w_full()
|
|
||||||
.bg(cx.theme().colors().title_bar_background)
|
|
||||||
.border()
|
|
||||||
.border_color(cx.theme().colors().border_variant)
|
|
||||||
.rounded_md()
|
|
||||||
.my_1()
|
|
||||||
.py_0p5()
|
|
||||||
.px_3()
|
|
||||||
.child(
|
|
||||||
List::new().empty_message("No projects.").children(
|
|
||||||
channel_store
|
|
||||||
.remote_projects_for_id(dev_server.channel_id)
|
|
||||||
.iter()
|
|
||||||
.filter_map(|remote_project| {
|
|
||||||
if remote_project.dev_server_id == dev_server.id {
|
|
||||||
Some(self.render_remote_project(remote_project, cx))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
// .child(div().ml_8().child(
|
|
||||||
// Button::new(("add-project", dev_server_id.0), "Add Project").on_click(cx.listener(
|
|
||||||
// move |this, _, cx| {
|
|
||||||
// this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
|
|
||||||
// dev_server_id,
|
|
||||||
// creating: None,
|
|
||||||
// remote_project: None,
|
|
||||||
// });
|
|
||||||
// cx.notify();
|
|
||||||
// },
|
|
||||||
// )),
|
|
||||||
// ))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_remote_project(
|
|
||||||
&mut self,
|
|
||||||
project: &RemoteProject,
|
|
||||||
_: &mut ViewContext<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(Icon::new(IconName::FileTree))
|
|
||||||
.child(Label::new(project.name.clone()))
|
|
||||||
.child(Label::new(format!("({})", project.path.clone())).color(Color::Muted))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_create_dev_server(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
let Mode::CreateDevServer(CreateDevServer {
|
|
||||||
creating,
|
|
||||||
dev_server,
|
|
||||||
}) = &self.mode
|
|
||||||
else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
|
|
||||||
self.dev_server_name_editor.update(cx, |editor, _| {
|
|
||||||
editor.set_read_only(creating.is_some() || dev_server.is_some())
|
|
||||||
});
|
|
||||||
v_flex()
|
|
||||||
.px_1()
|
|
||||||
.pt_0p5()
|
|
||||||
.gap_px()
|
|
||||||
.child(
|
|
||||||
v_flex().py_0p5().px_1().child(
|
|
||||||
h_flex()
|
|
||||||
.px_1()
|
|
||||||
.py_0p5()
|
|
||||||
.child(
|
|
||||||
IconButton::new("back", IconName::ArrowLeft)
|
|
||||||
.style(ButtonStyle::Transparent)
|
|
||||||
.on_click(cx.listener(|this, _: &gpui::ClickEvent, cx| {
|
|
||||||
this.mode = Mode::Default;
|
|
||||||
cx.notify();
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.child(Headline::new("Register dev server")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.ml_5()
|
|
||||||
.gap_2()
|
|
||||||
.child("Name")
|
|
||||||
.child(self.dev_server_name_editor.clone())
|
|
||||||
.on_action(
|
|
||||||
cx.listener(|this, _: &menu::Confirm, cx| this.create_dev_server(cx)),
|
|
||||||
)
|
|
||||||
.when(creating.is_none() && dev_server.is_none(), |div| {
|
|
||||||
div.child(
|
|
||||||
Button::new("create-dev-server", "Create").on_click(cx.listener(
|
|
||||||
move |this, _, cx| {
|
|
||||||
this.create_dev_server(cx);
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(creating.is_some() && dev_server.is_none(), |div| {
|
|
||||||
div.child(Button::new("create-dev-server", "Creating...").disabled(true))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.when_some(dev_server.clone(), |div, dev_server| {
|
|
||||||
let channel_store = self.channel_store.read(cx);
|
|
||||||
let status = channel_store
|
|
||||||
.find_dev_server_by_id(DevServerId(dev_server.dev_server_id))
|
|
||||||
.map(|server| server.status)
|
|
||||||
.unwrap_or(DevServerStatus::Offline);
|
|
||||||
let instructions = SharedString::from(format!(
|
|
||||||
"zed --dev-server-token {}",
|
|
||||||
dev_server.access_token
|
|
||||||
));
|
|
||||||
div.child(
|
|
||||||
v_flex()
|
|
||||||
.ml_8()
|
|
||||||
.gap_2()
|
|
||||||
.child(Label::new(format!(
|
|
||||||
"Please log into `{}` and run:",
|
|
||||||
dev_server.name
|
|
||||||
)))
|
|
||||||
.child(instructions.clone())
|
|
||||||
.child(
|
|
||||||
IconButton::new("copy-access-token", IconName::Copy)
|
|
||||||
.on_click(cx.listener(move |_, _, cx| {
|
|
||||||
cx.write_to_clipboard(ClipboardItem::new(
|
|
||||||
instructions.to_string(),
|
|
||||||
))
|
|
||||||
}))
|
|
||||||
.icon_size(IconSize::Small)
|
|
||||||
.tooltip(|cx| Tooltip::text("Copy access token", cx)),
|
|
||||||
)
|
|
||||||
.when(status == DevServerStatus::Offline, |this| {
|
|
||||||
this.child(Label::new("Waiting for connection..."))
|
|
||||||
})
|
|
||||||
.when(status == DevServerStatus::Online, |this| {
|
|
||||||
this.child(Label::new("Connection established! 🎊")).child(
|
|
||||||
Button::new("done", "Done").on_click(cx.listener(|this, _, cx| {
|
|
||||||
this.mode = Mode::Default;
|
|
||||||
cx.notify();
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
let channel_store = self.channel_store.read(cx);
|
|
||||||
let dev_servers = channel_store.dev_servers_for_id(self.channel_id);
|
|
||||||
// let dev_servers = Vec::new();
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.id("scroll-container")
|
|
||||||
.h_full()
|
|
||||||
.overflow_y_scroll()
|
|
||||||
.track_scroll(&self.scroll_handle)
|
|
||||||
.px_1()
|
|
||||||
.pt_0p5()
|
|
||||||
.gap_px()
|
|
||||||
.child(
|
|
||||||
ModalHeader::new("Manage Remote Project")
|
|
||||||
.child(Headline::new("Remote Projects").size(HeadlineSize::Small)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
ModalContent::new().child(
|
|
||||||
List::new()
|
|
||||||
.empty_message("No dev servers registered.")
|
|
||||||
.header(Some(
|
|
||||||
ListHeader::new("Dev Servers").end_slot(
|
|
||||||
Button::new("register-dev-server-button", "New Server")
|
|
||||||
.icon(IconName::Plus)
|
|
||||||
.icon_position(IconPosition::Start)
|
|
||||||
.tooltip(|cx| Tooltip::text("Register a new dev server", cx))
|
|
||||||
.on_click(cx.listener(|this, _, cx| {
|
|
||||||
this.mode = Mode::CreateDevServer(Default::default());
|
|
||||||
this.dev_server_name_editor
|
|
||||||
.read(cx)
|
|
||||||
.focus_handle(cx)
|
|
||||||
.focus(cx);
|
|
||||||
cx.notify();
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.children(dev_servers.iter().map(|dev_server| {
|
|
||||||
self.render_dev_server(dev_server, cx).into_any_element()
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_create_project(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
let Mode::CreateRemoteProject(CreateRemoteProject {
|
|
||||||
dev_server_id,
|
|
||||||
creating,
|
|
||||||
remote_project,
|
|
||||||
}) = &self.mode
|
|
||||||
else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
let channel_store = self.channel_store.read(cx);
|
|
||||||
let (dev_server_name, dev_server_status) = channel_store
|
|
||||||
.find_dev_server_by_id(*dev_server_id)
|
|
||||||
.map(|server| (server.name.clone(), server.status))
|
|
||||||
.unwrap_or((SharedString::from(""), DevServerStatus::Offline));
|
|
||||||
v_flex()
|
|
||||||
.px_1()
|
|
||||||
.pt_0p5()
|
|
||||||
.gap_px()
|
|
||||||
.child(
|
|
||||||
ModalHeader::new("Manage Remote Project")
|
|
||||||
.child(Headline::new("Manage Remote Projects")),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.py_0p5()
|
|
||||||
.px_1()
|
|
||||||
.child(div().px_1().py_0p5().child(
|
|
||||||
IconButton::new("back", IconName::ArrowLeft).on_click(cx.listener(
|
|
||||||
|this, _, cx| {
|
|
||||||
this.mode = Mode::Default;
|
|
||||||
cx.notify()
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
.child("Add Project..."),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.ml_5()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.id(("status", dev_server_id.0))
|
|
||||||
.relative()
|
|
||||||
.child(Icon::new(IconName::Server))
|
|
||||||
.child(div().absolute().bottom_0().left(rems_from_px(12.0)).child(
|
|
||||||
Indicator::dot().color(match dev_server_status {
|
|
||||||
DevServerStatus::Online => Color::Created,
|
|
||||||
DevServerStatus::Offline => Color::Deleted,
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
.tooltip(move |cx| {
|
|
||||||
Tooltip::text(
|
|
||||||
match dev_server_status {
|
|
||||||
DevServerStatus::Online => "Online",
|
|
||||||
DevServerStatus::Offline => "Offline",
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(dev_server_name.clone()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.ml_5()
|
|
||||||
.gap_2()
|
|
||||||
.child("Name")
|
|
||||||
.child(self.remote_project_name_editor.clone())
|
|
||||||
.on_action(cx.listener(|this, _: &menu::Confirm, cx| {
|
|
||||||
cx.focus_view(&this.remote_project_path_editor)
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.ml_5()
|
|
||||||
.gap_2()
|
|
||||||
.child("Path")
|
|
||||||
.child(self.remote_project_path_editor.clone())
|
|
||||||
.on_action(
|
|
||||||
cx.listener(|this, _: &menu::Confirm, cx| this.create_dev_server(cx)),
|
|
||||||
)
|
|
||||||
.when(creating.is_none() && remote_project.is_none(), |div| {
|
|
||||||
div.child(Button::new("create-remote-server", "Create").on_click({
|
|
||||||
let dev_server_id = *dev_server_id;
|
|
||||||
cx.listener(move |this, _, cx| {
|
|
||||||
this.create_remote_project(dev_server_id, cx)
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
.when(creating.is_some(), |div| {
|
|
||||||
div.child(Button::new("create-dev-server", "Creating...").disabled(true))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.when_some(remote_project.clone(), |div, remote_project| {
|
|
||||||
let channel_store = self.channel_store.read(cx);
|
|
||||||
let status = channel_store
|
|
||||||
.find_remote_project_by_id(RemoteProjectId(remote_project.id))
|
|
||||||
.map(|project| {
|
|
||||||
if project.project_id.is_some() {
|
|
||||||
DevServerStatus::Online
|
|
||||||
} else {
|
|
||||||
DevServerStatus::Offline
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or(DevServerStatus::Offline);
|
|
||||||
div.child(
|
|
||||||
v_flex()
|
|
||||||
.ml_5()
|
|
||||||
.ml_8()
|
|
||||||
.gap_2()
|
|
||||||
.when(status == DevServerStatus::Offline, |this| {
|
|
||||||
this.child(Label::new("Waiting for project..."))
|
|
||||||
})
|
|
||||||
.when(status == DevServerStatus::Online, |this| {
|
|
||||||
this.child(Label::new("Project online! 🎊")).child(
|
|
||||||
Button::new("done", "Done").on_click(cx.listener(|this, _, cx| {
|
|
||||||
this.mode = Mode::Default;
|
|
||||||
cx.notify();
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl ModalView for DevServerModal {}
|
|
||||||
|
|
||||||
impl FocusableView for DevServerModal {
|
|
||||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for DevServerModal {}
|
|
||||||
|
|
||||||
impl Render for DevServerModal {
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
div()
|
|
||||||
.track_focus(&self.focus_handle)
|
|
||||||
.elevation_3(cx)
|
|
||||||
.key_context("DevServerModal")
|
|
||||||
.on_action(cx.listener(Self::cancel))
|
|
||||||
.pb_4()
|
|
||||||
.w(rems(34.))
|
|
||||||
.min_h(rems(20.))
|
|
||||||
.max_h(rems(40.))
|
|
||||||
.child(match &self.mode {
|
|
||||||
Mode::Default => self.render_default(cx).into_any_element(),
|
|
||||||
Mode::CreateRemoteProject(_) => self.render_create_project(cx).into_any_element(),
|
|
||||||
Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -171,14 +171,17 @@ impl Render for CollabTitlebarItem {
|
|||||||
let room = room.read(cx);
|
let room = room.read(cx);
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
let is_local = project.is_local();
|
let is_local = project.is_local();
|
||||||
let is_shared = is_local && project.is_shared();
|
let is_remote_project = project.remote_project_id().is_some();
|
||||||
|
let is_shared = (is_local || is_remote_project) && project.is_shared();
|
||||||
let is_muted = room.is_muted();
|
let is_muted = room.is_muted();
|
||||||
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 can_use_microphone = room.can_use_microphone();
|
let can_use_microphone = room.can_use_microphone();
|
||||||
let can_share_projects = room.can_share_projects();
|
let can_share_projects = room.can_share_projects();
|
||||||
|
|
||||||
this.when(is_local && can_share_projects, |this| {
|
this.when(
|
||||||
|
(is_local || is_remote_project) && can_share_projects,
|
||||||
|
|this| {
|
||||||
this.child(
|
this.child(
|
||||||
Button::new(
|
Button::new(
|
||||||
"toggle_sharing",
|
"toggle_sharing",
|
||||||
@ -208,7 +211,8 @@ impl Render for CollabTitlebarItem {
|
|||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.child(
|
.child(
|
||||||
@ -406,7 +410,7 @@ impl CollabTitlebarItem {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl Element {
|
pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let name = {
|
let name = {
|
||||||
let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
|
let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
|
||||||
let worktree = worktree.read(cx);
|
let worktree = worktree.read(cx);
|
||||||
@ -423,15 +427,26 @@ impl CollabTitlebarItem {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
popover_menu("project_name_trigger")
|
|
||||||
.trigger(
|
|
||||||
Button::new("project_name_trigger", name)
|
Button::new("project_name_trigger", name)
|
||||||
.when(!is_project_selected, |b| b.color(Color::Muted))
|
.when(!is_project_selected, |b| b.color(Color::Muted))
|
||||||
.style(ButtonStyle::Subtle)
|
.style(ButtonStyle::Subtle)
|
||||||
.label_size(LabelSize::Small)
|
.label_size(LabelSize::Small)
|
||||||
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
|
.tooltip(move |cx| {
|
||||||
|
Tooltip::for_action(
|
||||||
|
"Recent Projects",
|
||||||
|
&recent_projects::OpenRecent {
|
||||||
|
create_new_window: false,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
.menu(move |cx| Some(Self::render_project_popover(workspace.clone(), cx)))
|
})
|
||||||
|
.on_click(cx.listener(move |_, _, cx| {
|
||||||
|
if let Some(workspace) = workspace.upgrade() {
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
RecentProjects::open(workspace, false, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
|
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
|
||||||
@ -607,17 +622,6 @@ impl CollabTitlebarItem {
|
|||||||
Some(view)
|
Some(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_project_popover(
|
|
||||||
workspace: WeakView<Workspace>,
|
|
||||||
cx: &mut WindowContext<'_>,
|
|
||||||
) -> View<RecentProjects> {
|
|
||||||
let view = RecentProjects::open_popover(workspace, cx);
|
|
||||||
|
|
||||||
let focus_handle = view.focus_handle(cx);
|
|
||||||
cx.focus(&focus_handle);
|
|
||||||
view
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_connection_status(
|
fn render_connection_status(
|
||||||
&self,
|
&self,
|
||||||
status: &client::Status,
|
status: &client::Status,
|
||||||
|
@ -81,6 +81,7 @@ impl FollowableItem for Editor {
|
|||||||
let mut buffers = futures::future::try_join_all(buffers?)
|
let mut buffers = futures::future::try_join_all(buffers?)
|
||||||
.await
|
.await
|
||||||
.debug_assert_ok("leaders don't share views for unshared buffers")?;
|
.debug_assert_ok("leaders don't share views for unshared buffers")?;
|
||||||
|
|
||||||
let editor = pane.update(&mut cx, |pane, cx| {
|
let editor = pane.update(&mut cx, |pane, cx| {
|
||||||
let mut editors = pane.items_of_type::<Self>();
|
let mut editors = pane.items_of_type::<Self>();
|
||||||
editors.find(|editor| {
|
editors.find(|editor| {
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::{user::UserStore, Client, ClientSettings, RemoteProjectId};
|
use client::RemoteProjectId;
|
||||||
|
use client::{user::UserStore, Client, ClientSettings};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, Task, WeakModel};
|
use gpui::{
|
||||||
|
AppContext, AsyncAppContext, BorrowAppContext, Context, Global, Model, ModelContext, Task,
|
||||||
|
WeakModel,
|
||||||
|
};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use postage::stream::Stream;
|
use postage::stream::Stream;
|
||||||
use project::Project;
|
use project::{Project, WorktreeSettings};
|
||||||
use rpc::{proto, TypedEnvelope};
|
use rpc::{proto, ErrorCode, TypedEnvelope};
|
||||||
use settings::Settings;
|
use settings::{Settings, SettingsStore};
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
|
|
||||||
pub struct DevServer {
|
pub struct DevServer {
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
app_state: AppState,
|
app_state: AppState,
|
||||||
|
remote_shutdown: bool,
|
||||||
projects: HashMap<RemoteProjectId, Model<Project>>,
|
projects: HashMap<RemoteProjectId, Model<Project>>,
|
||||||
_subscriptions: Vec<client::Subscription>,
|
_subscriptions: Vec<client::Subscription>,
|
||||||
_maintain_connection: Task<Option<()>>,
|
_maintain_connection: Task<Option<()>>,
|
||||||
@ -35,6 +40,15 @@ pub fn init(client: Arc<Client>, app_state: AppState, cx: &mut AppContext) {
|
|||||||
let dev_server = cx.new_model(|cx| DevServer::new(client.clone(), app_state, cx));
|
let dev_server = cx.new_model(|cx| DevServer::new(client.clone(), app_state, cx));
|
||||||
cx.set_global(GlobalDevServer(dev_server.clone()));
|
cx.set_global(GlobalDevServer(dev_server.clone()));
|
||||||
|
|
||||||
|
// Dev server cannot have any private files for now
|
||||||
|
cx.update_global(|store: &mut SettingsStore, _| {
|
||||||
|
let old_settings = store.get::<WorktreeSettings>(None);
|
||||||
|
store.override_global(WorktreeSettings {
|
||||||
|
private_files: Some(vec![]),
|
||||||
|
..old_settings.clone()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Set up a handler when the dev server is shut down by the user pressing Ctrl-C
|
// Set up a handler when the dev server is shut down by the user pressing Ctrl-C
|
||||||
let (tx, rx) = futures::channel::oneshot::channel();
|
let (tx, rx) = futures::channel::oneshot::channel();
|
||||||
set_ctrlc_handler(move || tx.send(()).log_err().unwrap()).log_err();
|
set_ctrlc_handler(move || tx.send(()).log_err().unwrap()).log_err();
|
||||||
@ -53,7 +67,7 @@ pub fn init(client: Arc<Client>, app_state: AppState, cx: &mut AppContext) {
|
|||||||
log::info!("Connected to {}", server_url);
|
log::info!("Connected to {}", server_url);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error connecting to {}: {}", server_url, e);
|
log::error!("Error connecting to '{}': {}", server_url, e);
|
||||||
cx.update(|cx| cx.quit()).log_err();
|
cx.update(|cx| cx.quit()).log_err();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,21 +103,33 @@ impl DevServer {
|
|||||||
|
|
||||||
DevServer {
|
DevServer {
|
||||||
_subscriptions: vec![
|
_subscriptions: vec![
|
||||||
client.add_message_handler(cx.weak_model(), Self::handle_dev_server_instructions)
|
client.add_message_handler(cx.weak_model(), Self::handle_dev_server_instructions),
|
||||||
|
client.add_request_handler(
|
||||||
|
cx.weak_model(),
|
||||||
|
Self::handle_validate_remote_project_request,
|
||||||
|
),
|
||||||
|
client.add_message_handler(cx.weak_model(), Self::handle_shutdown),
|
||||||
],
|
],
|
||||||
_maintain_connection: maintain_connection,
|
_maintain_connection: maintain_connection,
|
||||||
projects: Default::default(),
|
projects: Default::default(),
|
||||||
|
remote_shutdown: false,
|
||||||
app_state,
|
app_state,
|
||||||
client,
|
client,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app_will_quit(&mut self, _: &mut ModelContext<Self>) -> impl Future<Output = ()> {
|
fn app_will_quit(&mut self, _: &mut ModelContext<Self>) -> impl Future<Output = ()> {
|
||||||
let request = self.client.request(proto::ShutdownDevServer {});
|
let request = if self.remote_shutdown {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.client.request(proto::ShutdownDevServer {}))
|
||||||
|
};
|
||||||
async move {
|
async move {
|
||||||
|
if let Some(request) = request {
|
||||||
request.await.log_err();
|
request.await.log_err();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_dev_server_instructions(
|
async fn handle_dev_server_instructions(
|
||||||
this: Model<Self>,
|
this: Model<Self>,
|
||||||
@ -148,6 +174,35 @@ impl DevServer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_validate_remote_project_request(
|
||||||
|
this: Model<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::ValidateRemoteProjectRequest>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::Ack> {
|
||||||
|
let path = std::path::Path::new(&envelope.payload.path);
|
||||||
|
let fs = cx.read_model(&this, |this, _| this.app_state.fs.clone())?;
|
||||||
|
|
||||||
|
let path_exists = fs.is_dir(path).await;
|
||||||
|
if !path_exists {
|
||||||
|
return Err(anyhow::anyhow!(ErrorCode::RemoteProjectPathDoesNotExist))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(proto::Ack {})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_shutdown(
|
||||||
|
this: Model<Self>,
|
||||||
|
_envelope: TypedEnvelope<proto::ShutdownDevServer>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.remote_shutdown = true;
|
||||||
|
cx.quit();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn unshare_project(
|
fn unshare_project(
|
||||||
&mut self,
|
&mut self,
|
||||||
remote_project_id: &RemoteProjectId,
|
remote_project_id: &RemoteProjectId,
|
||||||
|
@ -11,6 +11,7 @@ pub struct HighlightedText {
|
|||||||
pub text: String,
|
pub text: String,
|
||||||
pub highlight_positions: Vec<usize>,
|
pub highlight_positions: Vec<usize>,
|
||||||
pub char_count: usize,
|
pub char_count: usize,
|
||||||
|
pub color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HighlightedText {
|
impl HighlightedText {
|
||||||
@ -39,13 +40,17 @@ impl HighlightedText {
|
|||||||
text,
|
text,
|
||||||
highlight_positions,
|
highlight_positions,
|
||||||
char_count,
|
char_count,
|
||||||
|
color: Color::Default,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn color(self, color: Color) -> Self {
|
||||||
|
Self { color, ..self }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for HighlightedText {
|
impl RenderOnce for HighlightedText {
|
||||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
fn render(self, _: &mut WindowContext) -> impl IntoElement {
|
||||||
HighlightedLabel::new(self.text, self.highlight_positions)
|
HighlightedLabel::new(self.text, self.highlight_positions).color(self.color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,8 @@ pub mod search_history;
|
|||||||
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::{
|
use client::{
|
||||||
proto, Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore,
|
proto, Client, Collaborator, PendingEntitySubscription, ProjectId, RemoteProjectId,
|
||||||
|
TypedEnvelope, UserStore,
|
||||||
};
|
};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
|
use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
|
||||||
@ -207,6 +208,7 @@ pub struct Project {
|
|||||||
prettier_instances: HashMap<PathBuf, PrettierInstance>,
|
prettier_instances: HashMap<PathBuf, PrettierInstance>,
|
||||||
tasks: Model<Inventory>,
|
tasks: Model<Inventory>,
|
||||||
hosted_project_id: Option<ProjectId>,
|
hosted_project_id: Option<ProjectId>,
|
||||||
|
remote_project_id: Option<client::RemoteProjectId>,
|
||||||
search_history: SearchHistory,
|
search_history: SearchHistory,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,6 +270,7 @@ enum ProjectClientState {
|
|||||||
capability: Capability,
|
capability: Capability,
|
||||||
remote_id: u64,
|
remote_id: u64,
|
||||||
replica_id: ReplicaId,
|
replica_id: ReplicaId,
|
||||||
|
in_room: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -723,6 +726,7 @@ impl Project {
|
|||||||
prettier_instances: HashMap::default(),
|
prettier_instances: HashMap::default(),
|
||||||
tasks,
|
tasks,
|
||||||
hosted_project_id: None,
|
hosted_project_id: None,
|
||||||
|
remote_project_id: None,
|
||||||
search_history: Self::new_search_history(),
|
search_history: Self::new_search_history(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -836,6 +840,7 @@ impl Project {
|
|||||||
capability: Capability::ReadWrite,
|
capability: Capability::ReadWrite,
|
||||||
remote_id,
|
remote_id,
|
||||||
replica_id,
|
replica_id,
|
||||||
|
in_room: response.payload.remote_project_id.is_none(),
|
||||||
},
|
},
|
||||||
supplementary_language_servers: HashMap::default(),
|
supplementary_language_servers: HashMap::default(),
|
||||||
language_servers: Default::default(),
|
language_servers: Default::default(),
|
||||||
@ -877,6 +882,10 @@ impl Project {
|
|||||||
prettier_instances: HashMap::default(),
|
prettier_instances: HashMap::default(),
|
||||||
tasks,
|
tasks,
|
||||||
hosted_project_id: None,
|
hosted_project_id: None,
|
||||||
|
remote_project_id: response
|
||||||
|
.payload
|
||||||
|
.remote_project_id
|
||||||
|
.map(|remote_project_id| RemoteProjectId(remote_project_id)),
|
||||||
search_history: Self::new_search_history(),
|
search_history: Self::new_search_history(),
|
||||||
};
|
};
|
||||||
this.set_role(role, cx);
|
this.set_role(role, cx);
|
||||||
@ -1235,6 +1244,10 @@ impl Project {
|
|||||||
self.hosted_project_id
|
self.hosted_project_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remote_project_id(&self) -> Option<RemoteProjectId> {
|
||||||
|
self.remote_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,
|
||||||
@ -1552,7 +1565,16 @@ impl Project {
|
|||||||
|
|
||||||
pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext<Self>) -> Result<()> {
|
pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||||
if !matches!(self.client_state, ProjectClientState::Local) {
|
if !matches!(self.client_state, ProjectClientState::Local) {
|
||||||
|
if let ProjectClientState::Remote { in_room, .. } = &mut self.client_state {
|
||||||
|
if *in_room || self.remote_project_id.is_none() {
|
||||||
return Err(anyhow!("project was already shared"));
|
return Err(anyhow!("project was already shared"));
|
||||||
|
} else {
|
||||||
|
*in_room = true;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!("project was already shared"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.client_subscriptions.push(
|
self.client_subscriptions.push(
|
||||||
self.client
|
self.client
|
||||||
@ -1763,8 +1785,15 @@ impl Project {
|
|||||||
|
|
||||||
fn unshare_internal(&mut self, cx: &mut AppContext) -> Result<()> {
|
fn unshare_internal(&mut self, cx: &mut AppContext) -> Result<()> {
|
||||||
if self.is_remote() {
|
if self.is_remote() {
|
||||||
|
if self.remote_project_id().is_some() {
|
||||||
|
if let ProjectClientState::Remote { in_room, .. } = &mut self.client_state {
|
||||||
|
*in_room = false
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
return Err(anyhow!("attempted to unshare a remote project"));
|
return Err(anyhow!("attempted to unshare a remote project"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let ProjectClientState::Shared { remote_id, .. } = self.client_state {
|
if let ProjectClientState::Shared { remote_id, .. } = self.client_state {
|
||||||
self.client_state = ProjectClientState::Local;
|
self.client_state = ProjectClientState::Local;
|
||||||
@ -6959,7 +6988,8 @@ impl Project {
|
|||||||
pub fn is_shared(&self) -> bool {
|
pub fn is_shared(&self) -> bool {
|
||||||
match &self.client_state {
|
match &self.client_state {
|
||||||
ProjectClientState::Shared { .. } => true,
|
ProjectClientState::Shared { .. } => true,
|
||||||
ProjectClientState::Local | ProjectClientState::Remote { .. } => false,
|
ProjectClientState::Local => false,
|
||||||
|
ProjectClientState::Remote { in_room, .. } => *in_room,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,14 +13,21 @@ path = "src/recent_projects.rs"
|
|||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
feature_flags.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
ordered-float.workspace = true
|
ordered-float.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
|
remote_projects.workspace = true
|
||||||
|
rpc.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
settings.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
theme.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
|
ui_text_field.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
mod remote_projects;
|
||||||
|
|
||||||
|
use feature_flags::FeatureFlagAppExt;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result,
|
Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||||
Subscription, Task, View, ViewContext, WeakView,
|
Subscription, Task, View, ViewContext, WeakView,
|
||||||
};
|
};
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
@ -8,11 +11,21 @@ use picker::{
|
|||||||
highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
|
highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
|
||||||
Picker, PickerDelegate,
|
Picker, PickerDelegate,
|
||||||
};
|
};
|
||||||
|
use remote_projects::RemoteProjects;
|
||||||
|
use rpc::proto::DevServerStatus;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{path::Path, sync::Arc};
|
use std::{
|
||||||
use ui::{prelude::*, tooltip_container, ListItem, ListItemSpacing, Tooltip};
|
path::{Path, PathBuf},
|
||||||
use util::paths::PathExt;
|
sync::Arc,
|
||||||
use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB};
|
};
|
||||||
|
use ui::{
|
||||||
|
prelude::*, tooltip_container, ButtonLike, IconWithIndicator, Indicator, KeyBinding, ListItem,
|
||||||
|
ListItemSpacing, Tooltip,
|
||||||
|
};
|
||||||
|
use util::{paths::PathExt, ResultExt};
|
||||||
|
use workspace::{
|
||||||
|
AppState, ModalView, SerializedWorkspaceLocation, Workspace, WorkspaceId, WORKSPACE_DB,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||||
pub struct OpenRecent {
|
pub struct OpenRecent {
|
||||||
@ -25,9 +38,12 @@ fn default_create_new_window() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gpui::impl_actions!(projects, [OpenRecent]);
|
gpui::impl_actions!(projects, [OpenRecent]);
|
||||||
|
gpui::actions!(projects, [OpenRemote]);
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.observe_new_views(RecentProjects::register).detach();
|
cx.observe_new_views(RecentProjects::register).detach();
|
||||||
|
cx.observe_new_views(remote_projects::RemoteProjects::register)
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RecentProjects {
|
pub struct RecentProjects {
|
||||||
@ -55,10 +71,11 @@ impl RecentProjects {
|
|||||||
let workspaces = WORKSPACE_DB
|
let workspaces = WORKSPACE_DB
|
||||||
.recent_workspaces_on_disk()
|
.recent_workspaces_on_disk()
|
||||||
.await
|
.await
|
||||||
|
.log_err()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
this.update(&mut cx, move |this, cx| {
|
this.update(&mut cx, move |this, cx| {
|
||||||
this.picker.update(cx, move |picker, cx| {
|
this.picker.update(cx, move |picker, cx| {
|
||||||
picker.delegate.workspaces = workspaces;
|
picker.delegate.set_workspaces(workspaces);
|
||||||
picker.update_matches(picker.query(cx), cx)
|
picker.update_matches(picker.query(cx), cx)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -75,9 +92,7 @@ impl RecentProjects {
|
|||||||
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||||
workspace.register_action(|workspace, open_recent: &OpenRecent, cx| {
|
workspace.register_action(|workspace, open_recent: &OpenRecent, cx| {
|
||||||
let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
|
let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
|
||||||
if let Some(handler) = Self::open(workspace, open_recent.create_new_window, cx) {
|
Self::open(workspace, open_recent.create_new_window, cx);
|
||||||
handler.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -89,24 +104,17 @@ impl RecentProjects {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open(
|
pub fn open(
|
||||||
_: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
create_new_window: bool,
|
create_new_window: bool,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) -> Option<Task<Result<()>>> {
|
) {
|
||||||
Some(cx.spawn(|workspace, mut cx| async move {
|
let weak = cx.view().downgrade();
|
||||||
workspace.update(&mut cx, |workspace, cx| {
|
|
||||||
let weak_workspace = cx.view().downgrade();
|
|
||||||
workspace.toggle_modal(cx, |cx| {
|
workspace.toggle_modal(cx, |cx| {
|
||||||
let delegate =
|
let delegate = RecentProjectsDelegate::new(weak, create_new_window, true);
|
||||||
RecentProjectsDelegate::new(weak_workspace, create_new_window, true);
|
|
||||||
|
|
||||||
let modal = Self::new(delegate, 34., cx);
|
let modal = Self::new(delegate, 34., cx);
|
||||||
modal
|
modal
|
||||||
});
|
})
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
|
pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
|
||||||
@ -143,13 +151,14 @@ impl Render for RecentProjects {
|
|||||||
|
|
||||||
pub struct RecentProjectsDelegate {
|
pub struct RecentProjectsDelegate {
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
workspaces: Vec<(WorkspaceId, WorkspaceLocation)>,
|
workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>,
|
||||||
selected_match_index: usize,
|
selected_match_index: usize,
|
||||||
matches: Vec<StringMatch>,
|
matches: Vec<StringMatch>,
|
||||||
render_paths: bool,
|
render_paths: bool,
|
||||||
create_new_window: bool,
|
create_new_window: bool,
|
||||||
// Flag to reset index when there is a new query vs not reset index when user delete an item
|
// Flag to reset index when there is a new query vs not reset index when user delete an item
|
||||||
reset_selected_match_index: bool,
|
reset_selected_match_index: bool,
|
||||||
|
has_any_remote_projects: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecentProjectsDelegate {
|
impl RecentProjectsDelegate {
|
||||||
@ -162,8 +171,17 @@ impl RecentProjectsDelegate {
|
|||||||
create_new_window,
|
create_new_window,
|
||||||
render_paths,
|
render_paths,
|
||||||
reset_selected_match_index: true,
|
reset_selected_match_index: true,
|
||||||
|
has_any_remote_projects: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_workspaces(&mut self, workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>) {
|
||||||
|
self.workspaces = workspaces;
|
||||||
|
self.has_any_remote_projects = self
|
||||||
|
.workspaces
|
||||||
|
.iter()
|
||||||
|
.any(|(_, location)| matches!(location, SerializedWorkspaceLocation::Remote(_)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
|
impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
|
||||||
impl PickerDelegate for RecentProjectsDelegate {
|
impl PickerDelegate for RecentProjectsDelegate {
|
||||||
@ -210,12 +228,18 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(id, (_, location))| {
|
.map(|(id, (_, location))| {
|
||||||
let combined_string = location
|
let combined_string = match location {
|
||||||
|
SerializedWorkspaceLocation::Local(paths) => paths
|
||||||
.paths()
|
.paths()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|path| path.compact().to_string_lossy().into_owned())
|
.map(|path| path.compact().to_string_lossy().into_owned())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("");
|
.join(""),
|
||||||
|
SerializedWorkspaceLocation::Remote(remote_project) => {
|
||||||
|
format!("{}{}", remote_project.dev_server_name, remote_project.path)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
StringMatchCandidate::new(id, combined_string)
|
StringMatchCandidate::new(id, combined_string)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@ -261,7 +285,9 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
if workspace.database_id() == *candidate_workspace_id {
|
if workspace.database_id() == *candidate_workspace_id {
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
} else {
|
} else {
|
||||||
let candidate_paths = candidate_workspace_location.paths().as_ref().clone();
|
match candidate_workspace_location {
|
||||||
|
SerializedWorkspaceLocation::Local(paths) => {
|
||||||
|
let paths = paths.paths().as_ref().clone();
|
||||||
if replace_current_window {
|
if replace_current_window {
|
||||||
cx.spawn(move |workspace, mut cx| async move {
|
cx.spawn(move |workspace, mut cx| async move {
|
||||||
let continue_replacing = workspace
|
let continue_replacing = workspace
|
||||||
@ -272,11 +298,8 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
if continue_replacing {
|
if continue_replacing {
|
||||||
workspace
|
workspace
|
||||||
.update(&mut cx, |workspace, cx| {
|
.update(&mut cx, |workspace, cx| {
|
||||||
workspace.open_workspace_for_paths(
|
workspace
|
||||||
true,
|
.open_workspace_for_paths(true, paths, cx)
|
||||||
candidate_paths,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
@ -284,7 +307,47 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
workspace.open_workspace_for_paths(false, candidate_paths, cx)
|
workspace.open_workspace_for_paths(false, paths, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//TODO support opening remote projects in the same window
|
||||||
|
SerializedWorkspaceLocation::Remote(remote_project) => {
|
||||||
|
let store = ::remote_projects::Store::global(cx).read(cx);
|
||||||
|
let Some(project_id) = store
|
||||||
|
.remote_project(remote_project.id)
|
||||||
|
.and_then(|p| p.project_id)
|
||||||
|
else {
|
||||||
|
let dev_server_name = remote_project.dev_server_name.clone();
|
||||||
|
return cx.spawn(|workspace, mut cx| async move {
|
||||||
|
let response =
|
||||||
|
cx.prompt(gpui::PromptLevel::Warning,
|
||||||
|
"Dev Server is offline",
|
||||||
|
Some(format!("Cannot connect to {}. To debug open the remote project settings.", dev_server_name).as_str()),
|
||||||
|
&["Ok", "Open Settings"]
|
||||||
|
).await?;
|
||||||
|
if response == 1 {
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
workspace.toggle_modal(cx, |cx| RemoteProjects::new(cx))
|
||||||
|
})?;
|
||||||
|
} else {
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
RecentProjects::open(workspace, true, cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if let Some(app_state) = AppState::global(cx).upgrade() {
|
||||||
|
let task =
|
||||||
|
workspace::join_remote_project(project_id, app_state, cx);
|
||||||
|
cx.spawn(|_, _| async move {
|
||||||
|
task.await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Task::ready(Err(anyhow::anyhow!("App state not found")))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -295,6 +358,14 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
|
|
||||||
fn dismissed(&mut self, _: &mut ViewContext<Picker<Self>>) {}
|
fn dismissed(&mut self, _: &mut ViewContext<Picker<Self>>) {}
|
||||||
|
|
||||||
|
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
|
||||||
|
if self.workspaces.is_empty() {
|
||||||
|
"Recently opened projects will show up here".into()
|
||||||
|
} else {
|
||||||
|
"No matches".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn render_match(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
@ -308,9 +379,30 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
let (workspace_id, location) = &self.workspaces[hit.candidate_id];
|
let (workspace_id, location) = &self.workspaces[hit.candidate_id];
|
||||||
let is_current_workspace = self.is_current_workspace(*workspace_id, cx);
|
let is_current_workspace = self.is_current_workspace(*workspace_id, cx);
|
||||||
|
|
||||||
|
let is_remote = matches!(location, SerializedWorkspaceLocation::Remote(_));
|
||||||
|
let dev_server_status =
|
||||||
|
if let SerializedWorkspaceLocation::Remote(remote_project) = location {
|
||||||
|
let store = ::remote_projects::Store::global(cx).read(cx);
|
||||||
|
Some(
|
||||||
|
store
|
||||||
|
.remote_project(remote_project.id)
|
||||||
|
.and_then(|p| store.dev_server(p.dev_server_id))
|
||||||
|
.map(|s| s.status)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let mut path_start_offset = 0;
|
let mut path_start_offset = 0;
|
||||||
let (match_labels, paths): (Vec<_>, Vec<_>) = location
|
let paths = match location {
|
||||||
.paths()
|
SerializedWorkspaceLocation::Local(paths) => paths.paths(),
|
||||||
|
SerializedWorkspaceLocation::Remote(remote_project) => Arc::new(vec![PathBuf::from(
|
||||||
|
format!("{}:{}", remote_project.dev_server_name, remote_project.path),
|
||||||
|
)]),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (match_labels, paths): (Vec<_>, Vec<_>) = paths
|
||||||
.iter()
|
.iter()
|
||||||
.map(|path| {
|
.map(|path| {
|
||||||
let path = path.compact();
|
let path = path.compact();
|
||||||
@ -323,22 +415,58 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
.unzip();
|
.unzip();
|
||||||
|
|
||||||
let highlighted_match = HighlightedMatchWithPaths {
|
let highlighted_match = HighlightedMatchWithPaths {
|
||||||
match_label: HighlightedText::join(match_labels.into_iter().flatten(), ", "),
|
match_label: HighlightedText::join(match_labels.into_iter().flatten(), ", ").color(
|
||||||
|
if matches!(dev_server_status, Some(DevServerStatus::Offline)) {
|
||||||
|
Color::Disabled
|
||||||
|
} else {
|
||||||
|
Color::Default
|
||||||
|
},
|
||||||
|
),
|
||||||
paths,
|
paths,
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
ListItem::new(ix)
|
ListItem::new(ix)
|
||||||
|
.selected(selected)
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.spacing(ListItemSpacing::Sparse)
|
||||||
.selected(selected)
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.flex_grow()
|
||||||
|
.gap_3()
|
||||||
|
.when(self.has_any_remote_projects, |this| {
|
||||||
|
this.child(if is_remote {
|
||||||
|
// if disabled, Color::Disabled
|
||||||
|
let indicator_color = match dev_server_status {
|
||||||
|
Some(DevServerStatus::Online) => Color::Created,
|
||||||
|
Some(DevServerStatus::Offline) => Color::Hidden,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
IconWithIndicator::new(
|
||||||
|
Icon::new(IconName::Server).color(Color::Muted),
|
||||||
|
Some(Indicator::dot()),
|
||||||
|
)
|
||||||
|
.indicator_color(indicator_color)
|
||||||
|
.indicator_border_color(if selected {
|
||||||
|
Some(cx.theme().colors().element_selected)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
} else {
|
||||||
|
Icon::new(IconName::Screen)
|
||||||
|
.color(Color::Muted)
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
})
|
||||||
.child({
|
.child({
|
||||||
let mut highlighted = highlighted_match.clone();
|
let mut highlighted = highlighted_match.clone();
|
||||||
if !self.render_paths {
|
if !self.render_paths {
|
||||||
highlighted.paths.clear();
|
highlighted.paths.clear();
|
||||||
}
|
}
|
||||||
highlighted.render(cx)
|
highlighted.render(cx)
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
.when(!is_current_workspace, |el| {
|
.when(!is_current_workspace, |el| {
|
||||||
let delete_button = div()
|
let delete_button = div()
|
||||||
.child(
|
.child(
|
||||||
@ -369,6 +497,39 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
|
||||||
|
if !cx.has_flag::<feature_flags::Remoting>() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(
|
||||||
|
h_flex()
|
||||||
|
.border_t_1()
|
||||||
|
.py_2()
|
||||||
|
.pr_2()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.justify_end()
|
||||||
|
.gap_4()
|
||||||
|
.child(
|
||||||
|
ButtonLike::new("remote")
|
||||||
|
.when_some(KeyBinding::for_action(&OpenRemote, cx), |button, key| {
|
||||||
|
button.child(key)
|
||||||
|
})
|
||||||
|
.child(Label::new("Connect remote…").color(Color::Muted))
|
||||||
|
.on_click(|_, cx| cx.dispatch_action(OpenRemote.boxed_clone())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
ButtonLike::new("local")
|
||||||
|
.when_some(
|
||||||
|
KeyBinding::for_action(&workspace::Open, cx),
|
||||||
|
|button, key| button.child(key),
|
||||||
|
)
|
||||||
|
.child(Label::new("Open folder…").color(Color::Muted))
|
||||||
|
.on_click(|_, cx| cx.dispatch_action(workspace::Open.boxed_clone())),
|
||||||
|
)
|
||||||
|
.into_any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute the highlighted text for the name and path
|
// Compute the highlighted text for the name and path
|
||||||
@ -406,6 +567,7 @@ fn highlights_for_path(
|
|||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
highlight_positions,
|
highlight_positions,
|
||||||
char_count,
|
char_count,
|
||||||
|
color: Color::Default,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -415,6 +577,7 @@ fn highlights_for_path(
|
|||||||
text: path_string.to_string(),
|
text: path_string.to_string(),
|
||||||
highlight_positions: path_positions,
|
highlight_positions: path_positions,
|
||||||
char_count: path_char_count,
|
char_count: path_char_count,
|
||||||
|
color: Color::Default,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -430,7 +593,7 @@ impl RecentProjectsDelegate {
|
|||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
this.update(&mut cx, move |picker, cx| {
|
this.update(&mut cx, move |picker, cx| {
|
||||||
picker.delegate.workspaces = workspaces;
|
picker.delegate.set_workspaces(workspaces);
|
||||||
picker.delegate.set_selected_index(ix - 1, cx);
|
picker.delegate.set_selected_index(ix - 1, cx);
|
||||||
picker.delegate.reset_selected_match_index = false;
|
picker.delegate.reset_selected_match_index = false;
|
||||||
picker.update_matches(picker.query(cx), cx)
|
picker.update_matches(picker.query(cx), cx)
|
||||||
@ -475,7 +638,7 @@ mod tests {
|
|||||||
use gpui::{TestAppContext, WindowHandle};
|
use gpui::{TestAppContext, WindowHandle};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use workspace::{open_paths, AppState};
|
use workspace::{open_paths, AppState, LocalPaths};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@ -539,10 +702,10 @@ mod tests {
|
|||||||
positions: Vec::new(),
|
positions: Vec::new(),
|
||||||
string: "fake candidate".to_string(),
|
string: "fake candidate".to_string(),
|
||||||
}];
|
}];
|
||||||
delegate.workspaces = vec![(
|
delegate.set_workspaces(vec![(
|
||||||
WorkspaceId::default(),
|
WorkspaceId::default(),
|
||||||
WorkspaceLocation::new(vec!["/test/path/"]),
|
LocalPaths::new(vec!["/test/path/"]).into(),
|
||||||
)];
|
)]);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
749
crates/recent_projects/src/remote_projects.rs
Normal file
749
crates/recent_projects/src/remote_projects.rs
Normal file
@ -0,0 +1,749 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use feature_flags::FeatureFlagViewExt;
|
||||||
|
use gpui::{
|
||||||
|
percentage, Action, Animation, AnimationExt, AppContext, ClipboardItem, DismissEvent,
|
||||||
|
EventEmitter, FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View,
|
||||||
|
ViewContext,
|
||||||
|
};
|
||||||
|
use remote_projects::{DevServer, DevServerId, RemoteProject, RemoteProjectId};
|
||||||
|
use rpc::{
|
||||||
|
proto::{self, CreateDevServerResponse, DevServerStatus},
|
||||||
|
ErrorCode, ErrorExt,
|
||||||
|
};
|
||||||
|
use settings::Settings;
|
||||||
|
use theme::ThemeSettings;
|
||||||
|
use ui::{prelude::*, Indicator, List, ListHeader, ListItem, ModalContent, ModalHeader, Tooltip};
|
||||||
|
use ui_text_field::{FieldLabelLayout, TextField};
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace};
|
||||||
|
|
||||||
|
use crate::OpenRemote;
|
||||||
|
|
||||||
|
pub struct RemoteProjects {
|
||||||
|
mode: Mode,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
scroll_handle: ScrollHandle,
|
||||||
|
remote_project_store: Model<remote_projects::Store>,
|
||||||
|
remote_project_path_input: View<TextField>,
|
||||||
|
dev_server_name_input: View<TextField>,
|
||||||
|
_subscription: gpui::Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct CreateDevServer {
|
||||||
|
creating: bool,
|
||||||
|
dev_server: Option<CreateDevServerResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CreateRemoteProject {
|
||||||
|
dev_server_id: DevServerId,
|
||||||
|
creating: bool,
|
||||||
|
remote_project: Option<proto::RemoteProject>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Mode {
|
||||||
|
Default,
|
||||||
|
CreateRemoteProject(CreateRemoteProject),
|
||||||
|
CreateDevServer(CreateDevServer),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RemoteProjects {
|
||||||
|
pub fn register(_: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||||
|
cx.observe_flag::<feature_flags::Remoting, _>(|enabled, workspace, _| {
|
||||||
|
if enabled {
|
||||||
|
workspace.register_action(|workspace, _: &OpenRemote, cx| {
|
||||||
|
workspace.toggle_modal(cx, |cx| Self::new(cx))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
let remote_project_path_input = cx.new_view(|cx| TextField::new(cx, "", "Project path"));
|
||||||
|
let dev_server_name_input =
|
||||||
|
cx.new_view(|cx| TextField::new(cx, "Name", "").with_label(FieldLabelLayout::Stacked));
|
||||||
|
|
||||||
|
let focus_handle = cx.focus_handle();
|
||||||
|
let remote_project_store = remote_projects::Store::global(cx);
|
||||||
|
|
||||||
|
let subscription = cx.observe(&remote_project_store, |_, _, cx| {
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
mode: Mode::Default,
|
||||||
|
focus_handle,
|
||||||
|
scroll_handle: ScrollHandle::new(),
|
||||||
|
remote_project_store,
|
||||||
|
remote_project_path_input,
|
||||||
|
dev_server_name_input,
|
||||||
|
_subscription: subscription,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_remote_project(
|
||||||
|
&mut self,
|
||||||
|
dev_server_id: DevServerId,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let path = self
|
||||||
|
.remote_project_path_input
|
||||||
|
.read(cx)
|
||||||
|
.editor()
|
||||||
|
.read(cx)
|
||||||
|
.text(cx)
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self
|
||||||
|
.remote_project_store
|
||||||
|
.read(cx)
|
||||||
|
.remote_projects_for_server(dev_server_id)
|
||||||
|
.iter()
|
||||||
|
.any(|p| p.path == path)
|
||||||
|
{
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
cx.prompt(
|
||||||
|
gpui::PromptLevel::Critical,
|
||||||
|
"Failed to create project",
|
||||||
|
Some(&format!(
|
||||||
|
"Project {} already exists for this dev server.",
|
||||||
|
path
|
||||||
|
)),
|
||||||
|
&["Ok"],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let create = {
|
||||||
|
let path = path.clone();
|
||||||
|
self.remote_project_store.update(cx, |store, cx| {
|
||||||
|
store.create_remote_project(dev_server_id, path, cx)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let result = create.await;
|
||||||
|
let remote_project = result.as_ref().ok().and_then(|r| r.remote_project.clone());
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
|
||||||
|
dev_server_id,
|
||||||
|
creating: false,
|
||||||
|
remote_project,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
result
|
||||||
|
})
|
||||||
|
.detach_and_prompt_err("Failed to create project", cx, move |e, _| {
|
||||||
|
match e.error_code() {
|
||||||
|
ErrorCode::DevServerOffline => Some(
|
||||||
|
"The dev server is offline. Please log in and check it is connected."
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
ErrorCode::RemoteProjectPathDoesNotExist => {
|
||||||
|
Some(format!("The path `{}` does not exist on the server.", path))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.remote_project_path_input.update(cx, |input, cx| {
|
||||||
|
input.editor().update(cx, |editor, cx| {
|
||||||
|
editor.set_text("", cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
self.mode = Mode::CreateRemoteProject(CreateRemoteProject {
|
||||||
|
dev_server_id,
|
||||||
|
creating: true,
|
||||||
|
remote_project: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_dev_server(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
let name = self
|
||||||
|
.dev_server_name_input
|
||||||
|
.read(cx)
|
||||||
|
.editor()
|
||||||
|
.read(cx)
|
||||||
|
.text(cx)
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dev_server = self
|
||||||
|
.remote_project_store
|
||||||
|
.update(cx, |store, cx| store.create_dev_server(name.clone(), cx));
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let result = dev_server.await;
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, _| match &result {
|
||||||
|
Ok(dev_server) => {
|
||||||
|
this.mode = Mode::CreateDevServer(CreateDevServer {
|
||||||
|
creating: false,
|
||||||
|
dev_server: Some(dev_server.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
this.mode = Mode::CreateDevServer(Default::default());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
result
|
||||||
|
})
|
||||||
|
.detach_and_prompt_err("Failed to create server", cx, |_, _| None);
|
||||||
|
|
||||||
|
self.mode = Mode::CreateDevServer(CreateDevServer {
|
||||||
|
creating: true,
|
||||||
|
dev_server: None,
|
||||||
|
});
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) {
|
||||||
|
let answer = cx.prompt(
|
||||||
|
gpui::PromptLevel::Info,
|
||||||
|
"Are you sure?",
|
||||||
|
Some("This will delete the dev server and all of its remote projects."),
|
||||||
|
&["Delete", "Cancel"],
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let answer = answer.await?;
|
||||||
|
|
||||||
|
if answer != 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.remote_project_store
|
||||||
|
.update(cx, |store, cx| store.delete_dev_server(id, cx))
|
||||||
|
})?
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||||
|
match self.mode {
|
||||||
|
Mode::Default => {}
|
||||||
|
Mode::CreateRemoteProject(CreateRemoteProject { dev_server_id, .. }) => {
|
||||||
|
self.create_remote_project(dev_server_id, cx);
|
||||||
|
}
|
||||||
|
Mode::CreateDevServer(_) => {
|
||||||
|
self.create_dev_server(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
|
match self.mode {
|
||||||
|
Mode::Default => cx.emit(DismissEvent),
|
||||||
|
Mode::CreateRemoteProject(_) | Mode::CreateDevServer(_) => {
|
||||||
|
self.mode = Mode::Default;
|
||||||
|
self.focus_handle(cx).focus(cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_dev_server(
|
||||||
|
&mut self,
|
||||||
|
dev_server: &DevServer,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let dev_server_id = dev_server.id;
|
||||||
|
let status = dev_server.status;
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.group("dev-server")
|
||||||
|
.justify_between()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id(("status", dev_server.id.0))
|
||||||
|
.relative()
|
||||||
|
.child(Icon::new(IconName::Server).size(IconSize::Small))
|
||||||
|
.child(
|
||||||
|
div().absolute().bottom_0().left(rems_from_px(8.0)).child(
|
||||||
|
Indicator::dot().color(match status {
|
||||||
|
DevServerStatus::Online => Color::Created,
|
||||||
|
DevServerStatus::Offline => Color::Hidden,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.tooltip(move |cx| {
|
||||||
|
Tooltip::text(
|
||||||
|
match status {
|
||||||
|
DevServerStatus::Online => "Online",
|
||||||
|
DevServerStatus::Offline => "Offline",
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(dev_server.name.clone())
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.visible_on_hover("dev-server")
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
IconButton::new("edit-dev-server", IconName::Pencil)
|
||||||
|
.disabled(true) //TODO implement this on the collab side
|
||||||
|
.tooltip(|cx| {
|
||||||
|
Tooltip::text("Coming Soon - Edit dev server", cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child({
|
||||||
|
let dev_server_id = dev_server.id;
|
||||||
|
IconButton::new("remove-dev-server", IconName::Trash)
|
||||||
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
|
this.delete_dev_server(dev_server_id, cx)
|
||||||
|
}))
|
||||||
|
.tooltip(|cx| Tooltip::text("Remove dev server", cx))
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex().gap_1().child(
|
||||||
|
IconButton::new(
|
||||||
|
("add-remote-project", dev_server_id.0),
|
||||||
|
IconName::Plus,
|
||||||
|
)
|
||||||
|
.tooltip(|cx| Tooltip::text("Add a remote project", cx))
|
||||||
|
.on_click(cx.listener(
|
||||||
|
move |this, _, cx| {
|
||||||
|
this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
|
||||||
|
dev_server_id,
|
||||||
|
creating: false,
|
||||||
|
remote_project: None,
|
||||||
|
});
|
||||||
|
this.remote_project_path_input
|
||||||
|
.read(cx)
|
||||||
|
.focus_handle(cx)
|
||||||
|
.focus(cx);
|
||||||
|
cx.notify();
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.bg(cx.theme().colors().title_bar_background) // todo: this should be distinct
|
||||||
|
.border()
|
||||||
|
.border_color(cx.theme().colors().border_variant)
|
||||||
|
.rounded_md()
|
||||||
|
.my_1()
|
||||||
|
.py_0p5()
|
||||||
|
.px_3()
|
||||||
|
.child(
|
||||||
|
List::new().empty_message("No projects.").children(
|
||||||
|
self.remote_project_store
|
||||||
|
.read(cx)
|
||||||
|
.remote_projects_for_server(dev_server.id)
|
||||||
|
.iter()
|
||||||
|
.map(|p| self.render_remote_project(p, cx)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_remote_project(
|
||||||
|
&mut self,
|
||||||
|
project: &RemoteProject,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let remote_project_id = project.id;
|
||||||
|
let project_id = project.project_id;
|
||||||
|
let is_online = project_id.is_some();
|
||||||
|
|
||||||
|
ListItem::new(("remote-project", remote_project_id.0))
|
||||||
|
.start_slot(Icon::new(IconName::FileTree).when(!is_online, |icon| icon.color(Color::Muted)))
|
||||||
|
.child(
|
||||||
|
Label::new(project.path.clone())
|
||||||
|
)
|
||||||
|
.on_click(cx.listener(move |_, _, cx| {
|
||||||
|
if let Some(project_id) = project_id {
|
||||||
|
if let Some(app_state) = AppState::global(cx).upgrade() {
|
||||||
|
workspace::join_remote_project(project_id, app_state, cx)
|
||||||
|
.detach_and_prompt_err("Could not join project", cx, |_, _| None)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
cx.prompt(gpui::PromptLevel::Critical, "This project is offline", Some("The `zed` instance running on this dev server is not connected. You will have to restart it."), &["Ok"]).await.log_err();
|
||||||
|
}).detach();
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_create_dev_server(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let Mode::CreateDevServer(CreateDevServer {
|
||||||
|
creating,
|
||||||
|
dev_server,
|
||||||
|
}) = &self.mode
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.dev_server_name_input.update(cx, |input, cx| {
|
||||||
|
input.set_disabled(*creating || dev_server.is_some(), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.id("scroll-container")
|
||||||
|
.h_full()
|
||||||
|
.overflow_y_scroll()
|
||||||
|
.track_scroll(&self.scroll_handle)
|
||||||
|
.px_1()
|
||||||
|
.pt_0p5()
|
||||||
|
.gap_px()
|
||||||
|
.child(
|
||||||
|
ModalHeader::new("remote-projects")
|
||||||
|
.show_back_button(true)
|
||||||
|
.child(Headline::new("New dev server").size(HeadlineSize::Small)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
ModalContent::new().child(
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.pb_2()
|
||||||
|
.items_end()
|
||||||
|
.w_full()
|
||||||
|
.px_2()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.pl_2()
|
||||||
|
.max_w(rems(16.))
|
||||||
|
.child(self.dev_server_name_input.clone()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.pl_1()
|
||||||
|
.pb(px(3.))
|
||||||
|
.when(!*creating && dev_server.is_none(), |div| {
|
||||||
|
div.child(Button::new("create-dev-server", "Create").on_click(
|
||||||
|
cx.listener(move |this, _, cx| {
|
||||||
|
this.create_dev_server(cx);
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.when(*creating && dev_server.is_none(), |div| {
|
||||||
|
div.child(
|
||||||
|
Button::new("create-dev-server", "Creating...")
|
||||||
|
.disabled(true),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.when(dev_server.is_none(), |div| {
|
||||||
|
div.px_2().child(Label::new("Once you have created a dev server, you will be given a command to run on the server to register it.").color(Color::Muted))
|
||||||
|
})
|
||||||
|
.when_some(dev_server.clone(), |div, dev_server| {
|
||||||
|
let status = self
|
||||||
|
.remote_project_store
|
||||||
|
.read(cx)
|
||||||
|
.dev_server_status(DevServerId(dev_server.dev_server_id));
|
||||||
|
|
||||||
|
let instructions = SharedString::from(format!(
|
||||||
|
"zed --dev-server-token {}",
|
||||||
|
dev_server.access_token
|
||||||
|
));
|
||||||
|
div.child(
|
||||||
|
v_flex()
|
||||||
|
.pl_2()
|
||||||
|
.pt_2()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
h_flex().justify_between().w_full()
|
||||||
|
.child(Label::new(format!(
|
||||||
|
"Please log into `{}` and run:",
|
||||||
|
dev_server.name
|
||||||
|
)))
|
||||||
|
.child(
|
||||||
|
Button::new("copy-access-token", "Copy Instructions")
|
||||||
|
.icon(Some(IconName::Copy))
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.on_click({
|
||||||
|
let instructions = instructions.clone();
|
||||||
|
cx.listener(move |_, _, cx| {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new(
|
||||||
|
instructions.to_string(),
|
||||||
|
))
|
||||||
|
})})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.bg(cx.theme().colors().title_bar_background) // todo: this should be distinct
|
||||||
|
.border()
|
||||||
|
.border_color(cx.theme().colors().border_variant)
|
||||||
|
.rounded_md()
|
||||||
|
.my_1()
|
||||||
|
.py_0p5()
|
||||||
|
.px_3()
|
||||||
|
.font(ThemeSettings::get_global(cx).buffer_font.family.clone())
|
||||||
|
.child(Label::new(instructions))
|
||||||
|
)
|
||||||
|
.when(status == DevServerStatus::Offline, |this| {
|
||||||
|
this.child(
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::ArrowCircle)
|
||||||
|
.size(IconSize::Medium)
|
||||||
|
.with_animation(
|
||||||
|
"arrow-circle",
|
||||||
|
Animation::new(Duration::from_secs(2)).repeat(),
|
||||||
|
|icon, delta| {
|
||||||
|
icon.transform(Transformation::rotate(percentage(delta)))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Label::new("Waiting for connection…"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(status == DevServerStatus::Online, |this| {
|
||||||
|
this.child(Label::new("🎊 Connection established!"))
|
||||||
|
.child(
|
||||||
|
h_flex().justify_end().child(
|
||||||
|
Button::new("done", "Done").on_click(cx.listener(
|
||||||
|
|_, _, cx| {
|
||||||
|
cx.dispatch_action(menu::Cancel.boxed_clone())
|
||||||
|
},
|
||||||
|
))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let dev_servers = self.remote_project_store.read(cx).dev_servers();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.id("scroll-container")
|
||||||
|
.h_full()
|
||||||
|
.overflow_y_scroll()
|
||||||
|
.track_scroll(&self.scroll_handle)
|
||||||
|
.px_1()
|
||||||
|
.pt_0p5()
|
||||||
|
.gap_px()
|
||||||
|
.child(
|
||||||
|
ModalHeader::new("remote-projects")
|
||||||
|
.show_dismiss_button(true)
|
||||||
|
.child(Headline::new("Remote Projects").size(HeadlineSize::Small)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
ModalContent::new().child(
|
||||||
|
List::new()
|
||||||
|
.empty_message("No dev servers registered.")
|
||||||
|
.header(Some(
|
||||||
|
ListHeader::new("Dev Servers").end_slot(
|
||||||
|
Button::new("register-dev-server-button", "New Server")
|
||||||
|
.icon(IconName::Plus)
|
||||||
|
.icon_position(IconPosition::Start)
|
||||||
|
.tooltip(|cx| Tooltip::text("Register a new dev server", cx))
|
||||||
|
.on_click(cx.listener(|this, _, cx| {
|
||||||
|
this.mode = Mode::CreateDevServer(Default::default());
|
||||||
|
|
||||||
|
this.dev_server_name_input.update(cx, |input, cx| {
|
||||||
|
input.editor().update(cx, |editor, cx| {
|
||||||
|
editor.set_text("", cx);
|
||||||
|
});
|
||||||
|
input.focus_handle(cx).focus(cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.children(dev_servers.iter().map(|dev_server| {
|
||||||
|
self.render_dev_server(dev_server, cx).into_any_element()
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_create_remote_project(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let Mode::CreateRemoteProject(CreateRemoteProject {
|
||||||
|
dev_server_id,
|
||||||
|
creating,
|
||||||
|
remote_project,
|
||||||
|
}) = &self.mode
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
let dev_server = self
|
||||||
|
.remote_project_store
|
||||||
|
.read(cx)
|
||||||
|
.dev_server(*dev_server_id)
|
||||||
|
.cloned();
|
||||||
|
|
||||||
|
let (dev_server_name, dev_server_status) = dev_server
|
||||||
|
.map(|server| (server.name, server.status))
|
||||||
|
.unwrap_or((SharedString::from(""), DevServerStatus::Offline));
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.px_1()
|
||||||
|
.pt_0p5()
|
||||||
|
.gap_px()
|
||||||
|
.child(
|
||||||
|
v_flex().py_0p5().px_1().child(
|
||||||
|
h_flex()
|
||||||
|
.px_1()
|
||||||
|
.py_0p5()
|
||||||
|
.child(
|
||||||
|
IconButton::new("back", IconName::ArrowLeft)
|
||||||
|
.style(ButtonStyle::Transparent)
|
||||||
|
.on_click(cx.listener(|_, _: &gpui::ClickEvent, cx| {
|
||||||
|
cx.dispatch_action(menu::Cancel.boxed_clone())
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(Headline::new("Add remote project").size(HeadlineSize::Small)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.ml_5()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id(("status", dev_server_id.0))
|
||||||
|
.relative()
|
||||||
|
.child(Icon::new(IconName::Server))
|
||||||
|
.child(div().absolute().bottom_0().left(rems_from_px(12.0)).child(
|
||||||
|
Indicator::dot().color(match dev_server_status {
|
||||||
|
DevServerStatus::Online => Color::Created,
|
||||||
|
DevServerStatus::Offline => Color::Hidden,
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
.tooltip(move |cx| {
|
||||||
|
Tooltip::text(
|
||||||
|
match dev_server_status {
|
||||||
|
DevServerStatus::Online => "Online",
|
||||||
|
DevServerStatus::Offline => "Offline",
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(dev_server_name.clone()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.ml_5()
|
||||||
|
.gap_2()
|
||||||
|
.child(self.remote_project_path_input.clone())
|
||||||
|
.when(!*creating && remote_project.is_none(), |div| {
|
||||||
|
div.child(Button::new("create-remote-server", "Create").on_click({
|
||||||
|
let dev_server_id = *dev_server_id;
|
||||||
|
cx.listener(move |this, _, cx| {
|
||||||
|
this.create_remote_project(dev_server_id, cx)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.when(*creating, |div| {
|
||||||
|
div.child(Button::new("create-dev-server", "Creating...").disabled(true))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.when_some(remote_project.clone(), |div, remote_project| {
|
||||||
|
let status = self
|
||||||
|
.remote_project_store
|
||||||
|
.read(cx)
|
||||||
|
.remote_project(RemoteProjectId(remote_project.id))
|
||||||
|
.map(|project| {
|
||||||
|
if project.project_id.is_some() {
|
||||||
|
DevServerStatus::Online
|
||||||
|
} else {
|
||||||
|
DevServerStatus::Offline
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(DevServerStatus::Offline);
|
||||||
|
div.child(
|
||||||
|
v_flex()
|
||||||
|
.ml_5()
|
||||||
|
.ml_8()
|
||||||
|
.gap_2()
|
||||||
|
.when(status == DevServerStatus::Offline, |this| {
|
||||||
|
this.child(Label::new("Waiting for project..."))
|
||||||
|
})
|
||||||
|
.when(status == DevServerStatus::Online, |this| {
|
||||||
|
this.child(Label::new("Project online! 🎊")).child(
|
||||||
|
Button::new("done", "Done").on_click(cx.listener(|_, _, cx| {
|
||||||
|
cx.dispatch_action(menu::Cancel.boxed_clone())
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ModalView for RemoteProjects {}
|
||||||
|
|
||||||
|
impl FocusableView for RemoteProjects {
|
||||||
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<DismissEvent> for RemoteProjects {}
|
||||||
|
|
||||||
|
impl Render for RemoteProjects {
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.track_focus(&self.focus_handle)
|
||||||
|
.elevation_3(cx)
|
||||||
|
.key_context("DevServerModal")
|
||||||
|
.on_action(cx.listener(Self::cancel))
|
||||||
|
.on_action(cx.listener(Self::confirm))
|
||||||
|
.on_mouse_down_out(cx.listener(|this, _, cx| {
|
||||||
|
if matches!(this.mode, Mode::Default) {
|
||||||
|
cx.emit(DismissEvent)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.pb_4()
|
||||||
|
.w(rems(34.))
|
||||||
|
.min_h(rems(20.))
|
||||||
|
.max_h(rems(40.))
|
||||||
|
.child(match &self.mode {
|
||||||
|
Mode::Default => self.render_default(cx).into_any_element(),
|
||||||
|
Mode::CreateRemoteProject(_) => {
|
||||||
|
self.render_create_remote_project(cx).into_any_element()
|
||||||
|
}
|
||||||
|
Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
23
crates/remote_projects/Cargo.toml
Normal file
23
crates/remote_projects/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "remote_projects"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/remote_projects.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
client.workspace = true
|
||||||
|
rpc.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
serde_json.workspace = true
|
186
crates/remote_projects/src/remote_projects.rs
Normal file
186
crates/remote_projects/src/remote_projects.rs
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, SharedString, Task};
|
||||||
|
use rpc::{
|
||||||
|
proto::{self, DevServerStatus},
|
||||||
|
TypedEnvelope,
|
||||||
|
};
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use client::{Client, ProjectId};
|
||||||
|
pub use client::{DevServerId, RemoteProjectId};
|
||||||
|
|
||||||
|
pub struct Store {
|
||||||
|
remote_projects: HashMap<RemoteProjectId, RemoteProject>,
|
||||||
|
dev_servers: HashMap<DevServerId, DevServer>,
|
||||||
|
_subscriptions: Vec<client::Subscription>,
|
||||||
|
client: Arc<Client>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RemoteProject {
|
||||||
|
pub id: RemoteProjectId,
|
||||||
|
pub project_id: Option<ProjectId>,
|
||||||
|
pub path: SharedString,
|
||||||
|
pub dev_server_id: DevServerId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<proto::RemoteProject> for RemoteProject {
|
||||||
|
fn from(project: proto::RemoteProject) -> Self {
|
||||||
|
Self {
|
||||||
|
id: RemoteProjectId(project.id),
|
||||||
|
project_id: project.project_id.map(|id| ProjectId(id)),
|
||||||
|
path: project.path.into(),
|
||||||
|
dev_server_id: DevServerId(project.dev_server_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DevServer {
|
||||||
|
pub id: DevServerId,
|
||||||
|
pub name: SharedString,
|
||||||
|
pub status: DevServerStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<proto::DevServer> for DevServer {
|
||||||
|
fn from(dev_server: proto::DevServer) -> Self {
|
||||||
|
Self {
|
||||||
|
id: DevServerId(dev_server.dev_server_id),
|
||||||
|
status: dev_server.status(),
|
||||||
|
name: dev_server.name.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlobalStore(Model<Store>);
|
||||||
|
|
||||||
|
impl Global for GlobalStore {}
|
||||||
|
|
||||||
|
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||||
|
let store = cx.new_model(|cx| Store::new(client, cx));
|
||||||
|
cx.set_global(GlobalStore(store));
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Store {
|
||||||
|
pub fn global(cx: &AppContext) -> Model<Store> {
|
||||||
|
cx.global::<GlobalStore>().0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(client: Arc<Client>, cx: &ModelContext<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
remote_projects: Default::default(),
|
||||||
|
dev_servers: Default::default(),
|
||||||
|
_subscriptions: vec![
|
||||||
|
client.add_message_handler(cx.weak_model(), Self::handle_remote_projects_update)
|
||||||
|
],
|
||||||
|
client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remote_projects_for_server(&self, id: DevServerId) -> Vec<RemoteProject> {
|
||||||
|
let mut projects: Vec<RemoteProject> = self
|
||||||
|
.remote_projects
|
||||||
|
.values()
|
||||||
|
.filter(|project| project.dev_server_id == id)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
projects.sort_by_key(|p| (p.path.clone(), p.id));
|
||||||
|
projects
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dev_servers(&self) -> Vec<DevServer> {
|
||||||
|
let mut dev_servers: Vec<DevServer> = self.dev_servers.values().cloned().collect();
|
||||||
|
dev_servers.sort_by_key(|d| (d.status == DevServerStatus::Offline, d.name.clone(), d.id));
|
||||||
|
dev_servers
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dev_server(&self, id: DevServerId) -> Option<&DevServer> {
|
||||||
|
self.dev_servers.get(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dev_server_status(&self, id: DevServerId) -> DevServerStatus {
|
||||||
|
self.dev_server(id)
|
||||||
|
.map(|server| server.status)
|
||||||
|
.unwrap_or(DevServerStatus::Offline)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remote_projects(&self) -> Vec<RemoteProject> {
|
||||||
|
let mut projects: Vec<RemoteProject> = self.remote_projects.values().cloned().collect();
|
||||||
|
projects.sort_by_key(|p| (p.path.clone(), p.id));
|
||||||
|
projects
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remote_project(&self, id: RemoteProjectId) -> Option<&RemoteProject> {
|
||||||
|
self.remote_projects.get(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_remote_projects_update(
|
||||||
|
this: Model<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::RemoteProjectsUpdate>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.dev_servers = envelope
|
||||||
|
.payload
|
||||||
|
.dev_servers
|
||||||
|
.into_iter()
|
||||||
|
.map(|dev_server| (DevServerId(dev_server.dev_server_id), dev_server.into()))
|
||||||
|
.collect();
|
||||||
|
this.remote_projects = envelope
|
||||||
|
.payload
|
||||||
|
.remote_projects
|
||||||
|
.into_iter()
|
||||||
|
.map(|project| (RemoteProjectId(project.id), project.into()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_remote_project(
|
||||||
|
&mut self,
|
||||||
|
dev_server_id: DevServerId,
|
||||||
|
path: String,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<proto::CreateRemoteProjectResponse>> {
|
||||||
|
let client = self.client.clone();
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
client
|
||||||
|
.request(proto::CreateRemoteProject {
|
||||||
|
dev_server_id: dev_server_id.0,
|
||||||
|
path,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_dev_server(
|
||||||
|
&mut self,
|
||||||
|
name: String,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<proto::CreateDevServerResponse>> {
|
||||||
|
let client = self.client.clone();
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
let result = client.request(proto::CreateDevServer { name }).await?;
|
||||||
|
Ok(result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_dev_server(
|
||||||
|
&mut self,
|
||||||
|
id: DevServerId,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let client = self.client.clone();
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
client
|
||||||
|
.request(proto::DeleteDevServer {
|
||||||
|
dev_server_id: id.0,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -233,6 +233,10 @@ message Envelope {
|
|||||||
JoinRemoteProject join_remote_project = 185;
|
JoinRemoteProject join_remote_project = 185;
|
||||||
RejoinRemoteProjects rejoin_remote_projects = 186;
|
RejoinRemoteProjects rejoin_remote_projects = 186;
|
||||||
RejoinRemoteProjectsResponse rejoin_remote_projects_response = 187;
|
RejoinRemoteProjectsResponse rejoin_remote_projects_response = 187;
|
||||||
|
|
||||||
|
RemoteProjectsUpdate remote_projects_update = 193;
|
||||||
|
ValidateRemoteProjectRequest validate_remote_project_request = 194; // Current max
|
||||||
|
DeleteDevServer delete_dev_server = 195;
|
||||||
}
|
}
|
||||||
|
|
||||||
reserved 158 to 161;
|
reserved 158 to 161;
|
||||||
@ -269,6 +273,8 @@ enum ErrorCode {
|
|||||||
UnsharedItem = 12;
|
UnsharedItem = 12;
|
||||||
NoSuchProject = 13;
|
NoSuchProject = 13;
|
||||||
DevServerAlreadyOnline = 14;
|
DevServerAlreadyOnline = 14;
|
||||||
|
DevServerOffline = 15;
|
||||||
|
RemoteProjectPathDoesNotExist = 16;
|
||||||
reserved 6;
|
reserved 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,6 +439,7 @@ message LiveKitConnectionInfo {
|
|||||||
message ShareProject {
|
message ShareProject {
|
||||||
uint64 room_id = 1;
|
uint64 room_id = 1;
|
||||||
repeated WorktreeMetadata worktrees = 2;
|
repeated WorktreeMetadata worktrees = 2;
|
||||||
|
optional uint64 remote_project_id = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ShareProjectResponse {
|
message ShareProjectResponse {
|
||||||
@ -457,8 +464,8 @@ message JoinHostedProject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message CreateRemoteProject {
|
message CreateRemoteProject {
|
||||||
uint64 channel_id = 1;
|
reserved 1;
|
||||||
string name = 2;
|
reserved 2;
|
||||||
uint64 dev_server_id = 3;
|
uint64 dev_server_id = 3;
|
||||||
string path = 4;
|
string path = 4;
|
||||||
}
|
}
|
||||||
@ -466,14 +473,18 @@ message CreateRemoteProjectResponse {
|
|||||||
RemoteProject remote_project = 1;
|
RemoteProject remote_project = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ValidateRemoteProjectRequest {
|
||||||
|
string path = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message CreateDevServer {
|
message CreateDevServer {
|
||||||
uint64 channel_id = 1;
|
reserved 1;
|
||||||
string name = 2;
|
string name = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateDevServerResponse {
|
message CreateDevServerResponse {
|
||||||
uint64 dev_server_id = 1;
|
uint64 dev_server_id = 1;
|
||||||
uint64 channel_id = 2;
|
reserved 2;
|
||||||
string access_token = 3;
|
string access_token = 3;
|
||||||
string name = 4;
|
string name = 4;
|
||||||
}
|
}
|
||||||
@ -481,6 +492,10 @@ message CreateDevServerResponse {
|
|||||||
message ShutdownDevServer {
|
message ShutdownDevServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message DeleteDevServer {
|
||||||
|
uint64 dev_server_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message ReconnectDevServer {
|
message ReconnectDevServer {
|
||||||
repeated UpdateProject reshared_projects = 1;
|
repeated UpdateProject reshared_projects = 1;
|
||||||
}
|
}
|
||||||
@ -493,6 +508,11 @@ message DevServerInstructions {
|
|||||||
repeated RemoteProject projects = 1;
|
repeated RemoteProject projects = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message RemoteProjectsUpdate {
|
||||||
|
repeated DevServer dev_servers = 1;
|
||||||
|
repeated RemoteProject remote_projects = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message ShareRemoteProject {
|
message ShareRemoteProject {
|
||||||
uint64 remote_project_id = 1;
|
uint64 remote_project_id = 1;
|
||||||
repeated WorktreeMetadata worktrees = 2;
|
repeated WorktreeMetadata worktrees = 2;
|
||||||
@ -509,6 +529,7 @@ message JoinProjectResponse {
|
|||||||
repeated Collaborator collaborators = 3;
|
repeated Collaborator collaborators = 3;
|
||||||
repeated LanguageServer language_servers = 4;
|
repeated LanguageServer language_servers = 4;
|
||||||
ChannelRole role = 6;
|
ChannelRole role = 6;
|
||||||
|
optional uint64 remote_project_id = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LeaveProject {
|
message LeaveProject {
|
||||||
@ -1131,11 +1152,10 @@ message UpdateChannels {
|
|||||||
repeated HostedProject hosted_projects = 10;
|
repeated HostedProject hosted_projects = 10;
|
||||||
repeated uint64 deleted_hosted_projects = 11;
|
repeated uint64 deleted_hosted_projects = 11;
|
||||||
|
|
||||||
repeated DevServer dev_servers = 12;
|
reserved 12;
|
||||||
repeated uint64 deleted_dev_servers = 13;
|
reserved 13;
|
||||||
|
reserved 14;
|
||||||
repeated RemoteProject remote_projects = 14;
|
reserved 15;
|
||||||
repeated uint64 deleted_remote_projects = 15;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message UpdateUserChannels {
|
message UpdateUserChannels {
|
||||||
@ -1174,14 +1194,14 @@ message HostedProject {
|
|||||||
message RemoteProject {
|
message RemoteProject {
|
||||||
uint64 id = 1;
|
uint64 id = 1;
|
||||||
optional uint64 project_id = 2;
|
optional uint64 project_id = 2;
|
||||||
uint64 channel_id = 3;
|
reserved 3;
|
||||||
string name = 4;
|
reserved 4;
|
||||||
uint64 dev_server_id = 5;
|
uint64 dev_server_id = 5;
|
||||||
string path = 6;
|
string path = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DevServer {
|
message DevServer {
|
||||||
uint64 channel_id = 1;
|
reserved 1;
|
||||||
uint64 dev_server_id = 2;
|
uint64 dev_server_id = 2;
|
||||||
string name = 3;
|
string name = 3;
|
||||||
DevServerStatus status = 4;
|
DevServerStatus status = 4;
|
||||||
|
@ -303,7 +303,7 @@ messages!(
|
|||||||
(SetRoomParticipantRole, Foreground),
|
(SetRoomParticipantRole, Foreground),
|
||||||
(BlameBuffer, Foreground),
|
(BlameBuffer, Foreground),
|
||||||
(BlameBufferResponse, Foreground),
|
(BlameBufferResponse, Foreground),
|
||||||
(CreateRemoteProject, Foreground),
|
(CreateRemoteProject, Background),
|
||||||
(CreateRemoteProjectResponse, Foreground),
|
(CreateRemoteProjectResponse, Foreground),
|
||||||
(CreateDevServer, Foreground),
|
(CreateDevServer, Foreground),
|
||||||
(CreateDevServerResponse, Foreground),
|
(CreateDevServerResponse, Foreground),
|
||||||
@ -317,6 +317,9 @@ messages!(
|
|||||||
(RejoinRemoteProjectsResponse, Foreground),
|
(RejoinRemoteProjectsResponse, Foreground),
|
||||||
(MultiLspQuery, Background),
|
(MultiLspQuery, Background),
|
||||||
(MultiLspQueryResponse, Background),
|
(MultiLspQueryResponse, Background),
|
||||||
|
(RemoteProjectsUpdate, Foreground),
|
||||||
|
(ValidateRemoteProjectRequest, Background),
|
||||||
|
(DeleteDevServer, Foreground)
|
||||||
);
|
);
|
||||||
|
|
||||||
request_messages!(
|
request_messages!(
|
||||||
@ -417,7 +420,9 @@ request_messages!(
|
|||||||
(JoinRemoteProject, JoinProjectResponse),
|
(JoinRemoteProject, JoinProjectResponse),
|
||||||
(RejoinRemoteProjects, RejoinRemoteProjectsResponse),
|
(RejoinRemoteProjects, RejoinRemoteProjectsResponse),
|
||||||
(ReconnectDevServer, ReconnectDevServerResponse),
|
(ReconnectDevServer, ReconnectDevServerResponse),
|
||||||
|
(ValidateRemoteProjectRequest, Ack),
|
||||||
(MultiLspQuery, MultiLspQueryResponse),
|
(MultiLspQuery, MultiLspQueryResponse),
|
||||||
|
(DeleteDevServer, Ack),
|
||||||
);
|
);
|
||||||
|
|
||||||
entity_messages!(
|
entity_messages!(
|
||||||
|
@ -105,7 +105,8 @@ impl Connection {
|
|||||||
let mut raw_statement = ptr::null_mut::<sqlite3_stmt>();
|
let mut raw_statement = ptr::null_mut::<sqlite3_stmt>();
|
||||||
let mut remaining_sql_ptr = ptr::null();
|
let mut remaining_sql_ptr = ptr::null();
|
||||||
|
|
||||||
let (res, offset, message, _conn) = if let Some(table_to_alter) = alter_table {
|
let (res, offset, message, _conn) =
|
||||||
|
if let Some((table_to_alter, column)) = alter_table {
|
||||||
// ALTER TABLE is a weird statement. When preparing the statement the table's
|
// ALTER TABLE is a weird statement. When preparing the statement the table's
|
||||||
// existence is checked *before* syntax checking any other part of the statement.
|
// existence is checked *before* syntax checking any other part of the statement.
|
||||||
// Therefore, we need to make sure that the table has been created before calling
|
// Therefore, we need to make sure that the table has been created before calling
|
||||||
@ -115,9 +116,7 @@ impl Connection {
|
|||||||
let temp_connection = Connection::open_memory(None);
|
let temp_connection = Connection::open_memory(None);
|
||||||
//This should always succeed, if it doesn't then you really should know about it
|
//This should always succeed, if it doesn't then you really should know about it
|
||||||
temp_connection
|
temp_connection
|
||||||
.exec(&format!(
|
.exec(&format!("CREATE TABLE {table_to_alter}({column})"))
|
||||||
"CREATE TABLE {table_to_alter}(__place_holder_column_for_syntax_checking)"
|
|
||||||
))
|
|
||||||
.unwrap()()
|
.unwrap()()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -203,7 +202,7 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_alter_table(remaining_sql_str: &str) -> Option<String> {
|
fn parse_alter_table(remaining_sql_str: &str) -> Option<(String, String)> {
|
||||||
let remaining_sql_str = remaining_sql_str.to_lowercase();
|
let remaining_sql_str = remaining_sql_str.to_lowercase();
|
||||||
if remaining_sql_str.starts_with("alter") {
|
if remaining_sql_str.starts_with("alter") {
|
||||||
if let Some(table_offset) = remaining_sql_str.find("table") {
|
if let Some(table_offset) = remaining_sql_str.find("table") {
|
||||||
@ -215,7 +214,19 @@ fn parse_alter_table(remaining_sql_str: &str) -> Option<String> {
|
|||||||
.take_while(|c| !c.is_whitespace())
|
.take_while(|c| !c.is_whitespace())
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
if !table_to_alter.is_empty() {
|
if !table_to_alter.is_empty() {
|
||||||
return Some(table_to_alter);
|
let column_name =
|
||||||
|
if let Some(rename_offset) = remaining_sql_str.find("rename column") {
|
||||||
|
let after_rename_offset = rename_offset + "rename column".len();
|
||||||
|
remaining_sql_str
|
||||||
|
.chars()
|
||||||
|
.skip(after_rename_offset)
|
||||||
|
.skip_while(|c| c.is_whitespace())
|
||||||
|
.take_while(|c| !c.is_whitespace())
|
||||||
|
.collect::<String>()
|
||||||
|
} else {
|
||||||
|
"__place_holder_column_for_syntax_checking".to_string()
|
||||||
|
};
|
||||||
|
return Some((table_to_alter, column_name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -320,6 +320,7 @@ impl<'a> Statement<'a> {
|
|||||||
this: &mut Statement,
|
this: &mut Statement,
|
||||||
callback: impl FnOnce(&mut Statement) -> Result<R>,
|
callback: impl FnOnce(&mut Statement) -> Result<R>,
|
||||||
) -> Result<R> {
|
) -> Result<R> {
|
||||||
|
println!("{:?}", std::any::type_name::<R>());
|
||||||
if this.step()? != StepResult::Row {
|
if this.step()? != StepResult::Row {
|
||||||
return Err(anyhow!("single called with query that returns no rows."));
|
return Err(anyhow!("single called with query that returns no rows."));
|
||||||
}
|
}
|
||||||
|
@ -330,6 +330,7 @@ impl PickerDelegate for TasksModalDelegate {
|
|||||||
text: hit.string.clone(),
|
text: hit.string.clone(),
|
||||||
highlight_positions: hit.positions.clone(),
|
highlight_positions: hit.positions.clone(),
|
||||||
char_count: hit.string.chars().count(),
|
char_count: hit.string.chars().count(),
|
||||||
|
color: Color::Default,
|
||||||
};
|
};
|
||||||
let icon = match source_kind {
|
let icon = match source_kind {
|
||||||
TaskSourceKind::UserInput => Some(Icon::new(IconName::Terminal)),
|
TaskSourceKind::UserInput => Some(Icon::new(IconName::Terminal)),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use gpui::{svg, IntoElement, Rems, Transformation};
|
use gpui::{svg, Hsla, IntoElement, Rems, Transformation};
|
||||||
use strum::EnumIter;
|
use strum::EnumIter;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::{prelude::*, Indicator};
|
||||||
|
|
||||||
#[derive(Default, PartialEq, Copy, Clone)]
|
#[derive(Default, PartialEq, Copy, Clone)]
|
||||||
pub enum IconSize {
|
pub enum IconSize {
|
||||||
@ -283,3 +283,63 @@ impl RenderOnce for Icon {
|
|||||||
.text_color(self.color.color(cx))
|
.text_color(self.color.color(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct IconWithIndicator {
|
||||||
|
icon: Icon,
|
||||||
|
indicator: Option<Indicator>,
|
||||||
|
indicator_border_color: Option<Hsla>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IconWithIndicator {
|
||||||
|
pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
|
||||||
|
Self {
|
||||||
|
icon,
|
||||||
|
indicator,
|
||||||
|
indicator_border_color: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
|
||||||
|
self.indicator = indicator;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn indicator_color(mut self, color: Color) -> Self {
|
||||||
|
if let Some(indicator) = self.indicator.as_mut() {
|
||||||
|
indicator.color = color;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
|
||||||
|
self.indicator_border_color = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for IconWithIndicator {
|
||||||
|
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||||
|
let indicator_border_color = self
|
||||||
|
.indicator_border_color
|
||||||
|
.unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
|
||||||
|
|
||||||
|
div()
|
||||||
|
.relative()
|
||||||
|
.child(self.icon)
|
||||||
|
.when_some(self.indicator, |this, indicator| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.w_2()
|
||||||
|
.h_2()
|
||||||
|
.border()
|
||||||
|
.border_color(indicator_border_color)
|
||||||
|
.rounded_full()
|
||||||
|
.neg_bottom_0p5()
|
||||||
|
.neg_right_1()
|
||||||
|
.child(indicator),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
use gpui::*;
|
use gpui::{prelude::FluentBuilder, *};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::{h_flex, IconButton, IconButtonShape, IconName, Label, LabelCommon, LabelSize};
|
use crate::{
|
||||||
|
h_flex, Clickable, IconButton, IconButtonShape, IconName, Label, LabelCommon, LabelSize,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ModalHeader {
|
pub struct ModalHeader {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
show_dismiss_button: bool,
|
||||||
|
show_back_button: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModalHeader {
|
impl ModalHeader {
|
||||||
@ -14,8 +18,20 @@ impl ModalHeader {
|
|||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
children: SmallVec::new(),
|
children: SmallVec::new(),
|
||||||
|
show_dismiss_button: false,
|
||||||
|
show_back_button: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn show_dismiss_button(mut self, show: bool) -> Self {
|
||||||
|
self.show_dismiss_button = show;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_back_button(mut self, show: bool) -> Self {
|
||||||
|
self.show_back_button = show;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParentElement for ModalHeader {
|
impl ParentElement for ModalHeader {
|
||||||
@ -31,9 +47,28 @@ impl RenderOnce for ModalHeader {
|
|||||||
.w_full()
|
.w_full()
|
||||||
.px_2()
|
.px_2()
|
||||||
.py_1p5()
|
.py_1p5()
|
||||||
|
.when(self.show_back_button, |this| {
|
||||||
|
this.child(
|
||||||
|
div().pr_1().child(
|
||||||
|
IconButton::new("back", IconName::ArrowLeft)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.on_click(|_, cx| {
|
||||||
|
cx.dispatch_action(menu::Cancel.boxed_clone());
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
.child(div().flex_1().children(self.children))
|
.child(div().flex_1().children(self.children))
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(IconButton::new("dismiss", IconName::Close).shape(IconButtonShape::Square))
|
.when(self.show_dismiss_button, |this| {
|
||||||
|
this.child(
|
||||||
|
IconButton::new("dismiss", IconName::Close)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.on_click(|_, cx| {
|
||||||
|
cx.dispatch_action(menu::Cancel.boxed_clone());
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +44,8 @@ pub struct TextField {
|
|||||||
start_icon: Option<IconName>,
|
start_icon: Option<IconName>,
|
||||||
/// The layout of the label relative to the text field.
|
/// The layout of the label relative to the text field.
|
||||||
with_label: FieldLabelLayout,
|
with_label: FieldLabelLayout,
|
||||||
|
/// Whether the text field is disabled.
|
||||||
|
disabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FocusableView for TextField {
|
impl FocusableView for TextField {
|
||||||
@ -72,6 +74,7 @@ impl TextField {
|
|||||||
editor,
|
editor,
|
||||||
start_icon: None,
|
start_icon: None,
|
||||||
with_label: FieldLabelLayout::Hidden,
|
with_label: FieldLabelLayout::Hidden,
|
||||||
|
disabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +87,16 @@ impl TextField {
|
|||||||
self.with_label = layout;
|
self.with_label = layout;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_disabled(&mut self, disabled: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
self.disabled = disabled;
|
||||||
|
self.editor
|
||||||
|
.update(cx, |editor, _| editor.set_read_only(disabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn editor(&self) -> &View<Editor> {
|
||||||
|
&self.editor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for TextField {
|
impl Render for TextField {
|
||||||
@ -91,17 +104,17 @@ impl Render for TextField {
|
|||||||
let settings = ThemeSettings::get_global(cx);
|
let settings = ThemeSettings::get_global(cx);
|
||||||
let theme_color = cx.theme().colors();
|
let theme_color = cx.theme().colors();
|
||||||
|
|
||||||
let style = TextFieldStyle {
|
let mut style = TextFieldStyle {
|
||||||
text_color: theme_color.text,
|
text_color: theme_color.text,
|
||||||
background_color: theme_color.ghost_element_background,
|
background_color: theme_color.ghost_element_background,
|
||||||
border_color: theme_color.border,
|
border_color: theme_color.border,
|
||||||
};
|
};
|
||||||
|
|
||||||
// if self.disabled {
|
if self.disabled {
|
||||||
// style.text_color = theme_color.text_disabled;
|
style.text_color = theme_color.text_disabled;
|
||||||
// style.background_color = theme_color.ghost_element_disabled;
|
style.background_color = theme_color.ghost_element_disabled;
|
||||||
// style.border_color = theme_color.border_disabled;
|
style.border_color = theme_color.border_disabled;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if self.error_message.is_some() {
|
// if self.error_message.is_some() {
|
||||||
// style.text_color = cx.theme().status().error;
|
// style.text_color = cx.theme().status().error;
|
||||||
@ -131,7 +144,15 @@ impl Render for TextField {
|
|||||||
.group("text-field")
|
.group("text-field")
|
||||||
.w_full()
|
.w_full()
|
||||||
.when(self.with_label == FieldLabelLayout::Stacked, |this| {
|
.when(self.with_label == FieldLabelLayout::Stacked, |this| {
|
||||||
this.child(Label::new(self.label.clone()).size(LabelSize::Default))
|
this.child(
|
||||||
|
Label::new(self.label.clone())
|
||||||
|
.size(LabelSize::Default)
|
||||||
|
.color(if self.disabled {
|
||||||
|
Color::Disabled
|
||||||
|
} else {
|
||||||
|
Color::Muted
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
v_flex().w_full().child(
|
v_flex().w_full().child(
|
||||||
|
@ -45,6 +45,7 @@ node_runtime.workspace = true
|
|||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
|
remote_projects.workspace = true
|
||||||
task.workspace = true
|
task.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
@ -513,8 +513,9 @@ impl<T: Item> ItemHandle for View<T> {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut event_subscription =
|
let mut event_subscription = Some(cx.subscribe(
|
||||||
Some(cx.subscribe(self, move |workspace, item, event, cx| {
|
self,
|
||||||
|
move |workspace, item: View<T>, event, cx| {
|
||||||
let pane = if let Some(pane) = workspace
|
let pane = if let Some(pane) = workspace
|
||||||
.panes_by_item
|
.panes_by_item
|
||||||
.get(&item.item_id())
|
.get(&item.item_id())
|
||||||
@ -575,7 +576,8 @@ impl<T: Item> ItemHandle for View<T> {
|
|||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
});
|
});
|
||||||
}));
|
},
|
||||||
|
));
|
||||||
|
|
||||||
cx.on_blur(&self.focus_handle(cx), move |workspace, cx| {
|
cx.on_blur(&self.focus_handle(cx), move |workspace, cx| {
|
||||||
if WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange {
|
if WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange {
|
||||||
|
@ -3,6 +3,7 @@ pub mod model;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
|
use client::RemoteProjectId;
|
||||||
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
|
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
|
||||||
use gpui::{point, size, Axis, Bounds};
|
use gpui::{point, size, Axis, Bounds};
|
||||||
|
|
||||||
@ -17,11 +18,11 @@ use uuid::Uuid;
|
|||||||
use crate::WorkspaceId;
|
use crate::WorkspaceId;
|
||||||
|
|
||||||
use model::{
|
use model::{
|
||||||
GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
|
GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
|
||||||
WorkspaceLocation,
|
SerializedWorkspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::model::DockStructure;
|
use self::model::{DockStructure, SerializedRemoteProject, SerializedWorkspaceLocation};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
|
pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
|
||||||
@ -125,7 +126,7 @@ define_connection! {
|
|||||||
//
|
//
|
||||||
// workspaces(
|
// workspaces(
|
||||||
// workspace_id: usize, // Primary key for workspaces
|
// workspace_id: usize, // Primary key for workspaces
|
||||||
// workspace_location: Bincode<Vec<PathBuf>>,
|
// local_paths: Bincode<Vec<PathBuf>>,
|
||||||
// dock_visible: bool, // Deprecated
|
// dock_visible: bool, // Deprecated
|
||||||
// dock_anchor: DockAnchor, // Deprecated
|
// dock_anchor: DockAnchor, // Deprecated
|
||||||
// dock_pane: Option<usize>, // Deprecated
|
// dock_pane: Option<usize>, // Deprecated
|
||||||
@ -289,6 +290,15 @@ define_connection! {
|
|||||||
sql!(
|
sql!(
|
||||||
ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
|
ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
|
||||||
),
|
),
|
||||||
|
sql!(
|
||||||
|
CREATE TABLE remote_projects (
|
||||||
|
remote_project_id INTEGER NOT NULL UNIQUE,
|
||||||
|
path TEXT,
|
||||||
|
dev_server_name TEXT
|
||||||
|
);
|
||||||
|
ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
|
||||||
|
ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,13 +310,23 @@ impl WorkspaceDb {
|
|||||||
&self,
|
&self,
|
||||||
worktree_roots: &[P],
|
worktree_roots: &[P],
|
||||||
) -> Option<SerializedWorkspace> {
|
) -> Option<SerializedWorkspace> {
|
||||||
let workspace_location: WorkspaceLocation = worktree_roots.into();
|
let local_paths = LocalPaths::new(worktree_roots);
|
||||||
|
|
||||||
// Note that we re-assign the workspace_id here in case it's empty
|
// Note that we re-assign the workspace_id here in case it's empty
|
||||||
// and we've grabbed the most recent workspace
|
// and we've grabbed the most recent workspace
|
||||||
let (workspace_id, workspace_location, bounds, display, fullscreen, centered_layout, docks): (
|
let (
|
||||||
|
workspace_id,
|
||||||
|
local_paths,
|
||||||
|
remote_project_id,
|
||||||
|
bounds,
|
||||||
|
display,
|
||||||
|
fullscreen,
|
||||||
|
centered_layout,
|
||||||
|
docks,
|
||||||
|
): (
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
WorkspaceLocation,
|
Option<LocalPaths>,
|
||||||
|
Option<u64>,
|
||||||
Option<SerializedWindowsBounds>,
|
Option<SerializedWindowsBounds>,
|
||||||
Option<Uuid>,
|
Option<Uuid>,
|
||||||
Option<bool>,
|
Option<bool>,
|
||||||
@ -316,7 +336,8 @@ impl WorkspaceDb {
|
|||||||
.select_row_bound(sql! {
|
.select_row_bound(sql! {
|
||||||
SELECT
|
SELECT
|
||||||
workspace_id,
|
workspace_id,
|
||||||
workspace_location,
|
local_paths,
|
||||||
|
remote_project_id,
|
||||||
window_state,
|
window_state,
|
||||||
window_x,
|
window_x,
|
||||||
window_y,
|
window_y,
|
||||||
@ -335,16 +356,34 @@ impl WorkspaceDb {
|
|||||||
bottom_dock_active_panel,
|
bottom_dock_active_panel,
|
||||||
bottom_dock_zoom
|
bottom_dock_zoom
|
||||||
FROM workspaces
|
FROM workspaces
|
||||||
WHERE workspace_location = ?
|
WHERE local_paths = ?
|
||||||
})
|
})
|
||||||
.and_then(|mut prepared_statement| (prepared_statement)(&workspace_location))
|
.and_then(|mut prepared_statement| (prepared_statement)(&local_paths))
|
||||||
.context("No workspaces found")
|
.context("No workspaces found")
|
||||||
.warn_on_err()
|
.warn_on_err()
|
||||||
.flatten()?;
|
.flatten()?;
|
||||||
|
|
||||||
|
let location = if let Some(remote_project_id) = remote_project_id {
|
||||||
|
let remote_project: SerializedRemoteProject = self
|
||||||
|
.select_row_bound(sql! {
|
||||||
|
SELECT remote_project_id, path, dev_server_name
|
||||||
|
FROM remote_projects
|
||||||
|
WHERE remote_project_id = ?
|
||||||
|
})
|
||||||
|
.and_then(|mut prepared_statement| (prepared_statement)(remote_project_id))
|
||||||
|
.context("No remote project found")
|
||||||
|
.warn_on_err()
|
||||||
|
.flatten()?;
|
||||||
|
SerializedWorkspaceLocation::Remote(remote_project)
|
||||||
|
} else if let Some(local_paths) = local_paths {
|
||||||
|
SerializedWorkspaceLocation::Local(local_paths)
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
Some(SerializedWorkspace {
|
Some(SerializedWorkspace {
|
||||||
id: workspace_id,
|
id: workspace_id,
|
||||||
location: workspace_location.clone(),
|
location,
|
||||||
center_group: self
|
center_group: self
|
||||||
.get_center_pane_group(workspace_id)
|
.get_center_pane_group(workspace_id)
|
||||||
.context("Getting center group")
|
.context("Getting center group")
|
||||||
@ -368,16 +407,18 @@ impl WorkspaceDb {
|
|||||||
DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
|
DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
|
||||||
.context("Clearing old panes")?;
|
.context("Clearing old panes")?;
|
||||||
|
|
||||||
|
match workspace.location {
|
||||||
|
SerializedWorkspaceLocation::Local(local_paths) => {
|
||||||
conn.exec_bound(sql!(
|
conn.exec_bound(sql!(
|
||||||
DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
|
DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ?
|
||||||
))?((&workspace.location, workspace.id))
|
))?((&local_paths, workspace.id))
|
||||||
.context("clearing out old locations")?;
|
.context("clearing out old locations")?;
|
||||||
|
|
||||||
// Upsert
|
// Upsert
|
||||||
conn.exec_bound(sql!(
|
conn.exec_bound(sql!(
|
||||||
INSERT INTO workspaces(
|
INSERT INTO workspaces(
|
||||||
workspace_id,
|
workspace_id,
|
||||||
workspace_location,
|
local_paths,
|
||||||
left_dock_visible,
|
left_dock_visible,
|
||||||
left_dock_active_panel,
|
left_dock_active_panel,
|
||||||
left_dock_zoom,
|
left_dock_zoom,
|
||||||
@ -392,7 +433,7 @@ impl WorkspaceDb {
|
|||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
|
||||||
ON CONFLICT DO
|
ON CONFLICT DO
|
||||||
UPDATE SET
|
UPDATE SET
|
||||||
workspace_location = ?2,
|
local_paths = ?2,
|
||||||
left_dock_visible = ?3,
|
left_dock_visible = ?3,
|
||||||
left_dock_active_panel = ?4,
|
left_dock_active_panel = ?4,
|
||||||
left_dock_zoom = ?5,
|
left_dock_zoom = ?5,
|
||||||
@ -403,8 +444,65 @@ impl WorkspaceDb {
|
|||||||
bottom_dock_active_panel = ?10,
|
bottom_dock_active_panel = ?10,
|
||||||
bottom_dock_zoom = ?11,
|
bottom_dock_zoom = ?11,
|
||||||
timestamp = CURRENT_TIMESTAMP
|
timestamp = CURRENT_TIMESTAMP
|
||||||
))?((workspace.id, &workspace.location, workspace.docks))
|
))?((workspace.id, &local_paths, workspace.docks))
|
||||||
.context("Updating workspace")?;
|
.context("Updating workspace")?;
|
||||||
|
}
|
||||||
|
SerializedWorkspaceLocation::Remote(remote_project) => {
|
||||||
|
conn.exec_bound(sql!(
|
||||||
|
DELETE FROM workspaces WHERE remote_project_id = ? AND workspace_id != ?
|
||||||
|
))?((remote_project.id.0, workspace.id))
|
||||||
|
.context("clearing out old locations")?;
|
||||||
|
|
||||||
|
conn.exec_bound(sql!(
|
||||||
|
INSERT INTO remote_projects(
|
||||||
|
remote_project_id,
|
||||||
|
path,
|
||||||
|
dev_server_name
|
||||||
|
) VALUES (?1, ?2, ?3)
|
||||||
|
ON CONFLICT DO
|
||||||
|
UPDATE SET
|
||||||
|
path = ?2,
|
||||||
|
dev_server_name = ?3
|
||||||
|
))?(&remote_project)?;
|
||||||
|
|
||||||
|
// Upsert
|
||||||
|
conn.exec_bound(sql!(
|
||||||
|
INSERT INTO workspaces(
|
||||||
|
workspace_id,
|
||||||
|
remote_project_id,
|
||||||
|
left_dock_visible,
|
||||||
|
left_dock_active_panel,
|
||||||
|
left_dock_zoom,
|
||||||
|
right_dock_visible,
|
||||||
|
right_dock_active_panel,
|
||||||
|
right_dock_zoom,
|
||||||
|
bottom_dock_visible,
|
||||||
|
bottom_dock_active_panel,
|
||||||
|
bottom_dock_zoom,
|
||||||
|
timestamp
|
||||||
|
)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT DO
|
||||||
|
UPDATE SET
|
||||||
|
remote_project_id = ?2,
|
||||||
|
left_dock_visible = ?3,
|
||||||
|
left_dock_active_panel = ?4,
|
||||||
|
left_dock_zoom = ?5,
|
||||||
|
right_dock_visible = ?6,
|
||||||
|
right_dock_active_panel = ?7,
|
||||||
|
right_dock_zoom = ?8,
|
||||||
|
bottom_dock_visible = ?9,
|
||||||
|
bottom_dock_active_panel = ?10,
|
||||||
|
bottom_dock_zoom = ?11,
|
||||||
|
timestamp = CURRENT_TIMESTAMP
|
||||||
|
))?((
|
||||||
|
workspace.id,
|
||||||
|
remote_project.id.0,
|
||||||
|
workspace.docks,
|
||||||
|
))
|
||||||
|
.context("Updating workspace")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save center pane group
|
// Save center pane group
|
||||||
Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
|
Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
|
||||||
@ -424,22 +522,41 @@ impl WorkspaceDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query! {
|
query! {
|
||||||
fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
|
fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, Option<u64>)>> {
|
||||||
SELECT workspace_id, workspace_location
|
SELECT workspace_id, local_paths, remote_project_id
|
||||||
FROM workspaces
|
FROM workspaces
|
||||||
WHERE workspace_location IS NOT NULL
|
WHERE local_paths IS NOT NULL OR remote_project_id IS NOT NULL
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query! {
|
query! {
|
||||||
pub fn last_window() -> Result<(Option<Uuid>, Option<SerializedWindowsBounds>, Option<bool>)> {
|
fn remote_projects() -> Result<Vec<SerializedRemoteProject>> {
|
||||||
SELECT display, window_state, window_x, window_y, window_width, window_height, fullscreen
|
SELECT remote_project_id, path, dev_server_name
|
||||||
|
FROM remote_projects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn last_window(
|
||||||
|
&self,
|
||||||
|
) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowsBounds>, Option<bool>)> {
|
||||||
|
let mut prepared_query =
|
||||||
|
self.select::<(Option<Uuid>, Option<SerializedWindowsBounds>, Option<bool>)>(sql!(
|
||||||
|
SELECT
|
||||||
|
display,
|
||||||
|
window_state, window_x, window_y, window_width, window_height,
|
||||||
|
fullscreen
|
||||||
FROM workspaces
|
FROM workspaces
|
||||||
WHERE workspace_location IS NOT NULL
|
WHERE local_paths
|
||||||
|
IS NOT NULL
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
}
|
))?;
|
||||||
|
let result = prepared_query()?;
|
||||||
|
Ok(result
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap_or_else(|| (None, None, None)))
|
||||||
}
|
}
|
||||||
|
|
||||||
query! {
|
query! {
|
||||||
@ -451,14 +568,29 @@ impl WorkspaceDb {
|
|||||||
|
|
||||||
// Returns the recent locations which are still valid on disk and deletes ones which no longer
|
// Returns the recent locations which are still valid on disk and deletes ones which no longer
|
||||||
// exist.
|
// exist.
|
||||||
pub async fn recent_workspaces_on_disk(&self) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
|
pub async fn recent_workspaces_on_disk(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut delete_tasks = Vec::new();
|
let mut delete_tasks = Vec::new();
|
||||||
for (id, location) in self.recent_workspaces()? {
|
let remote_projects = self.remote_projects()?;
|
||||||
|
|
||||||
|
for (id, location, remote_project_id) in self.recent_workspaces()? {
|
||||||
|
if let Some(remote_project_id) = remote_project_id.map(RemoteProjectId) {
|
||||||
|
if let Some(remote_project) =
|
||||||
|
remote_projects.iter().find(|rp| rp.id == remote_project_id)
|
||||||
|
{
|
||||||
|
result.push((id, remote_project.clone().into()));
|
||||||
|
} else {
|
||||||
|
delete_tasks.push(self.delete_workspace_by_id(id));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if location.paths().iter().all(|path| path.exists())
|
if location.paths().iter().all(|path| path.exists())
|
||||||
&& location.paths().iter().any(|path| path.is_dir())
|
&& location.paths().iter().any(|path| path.is_dir())
|
||||||
{
|
{
|
||||||
result.push((id, location));
|
result.push((id, location.into()));
|
||||||
} else {
|
} else {
|
||||||
delete_tasks.push(self.delete_workspace_by_id(id));
|
delete_tasks.push(self.delete_workspace_by_id(id));
|
||||||
}
|
}
|
||||||
@ -468,13 +600,16 @@ impl WorkspaceDb {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn last_workspace(&self) -> Result<Option<WorkspaceLocation>> {
|
pub async fn last_workspace(&self) -> Result<Option<LocalPaths>> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.recent_workspaces_on_disk()
|
.recent_workspaces_on_disk()
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.next()
|
.filter_map(|(_, location)| match location {
|
||||||
.map(|(_, location)| location))
|
SerializedWorkspaceLocation::Local(local_paths) => Some(local_paths),
|
||||||
|
SerializedWorkspaceLocation::Remote(_) => None,
|
||||||
|
})
|
||||||
|
.next())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
|
fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
|
||||||
@ -774,7 +909,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut workspace_1 = SerializedWorkspace {
|
let mut workspace_1 = SerializedWorkspace {
|
||||||
id: WorkspaceId(1),
|
id: WorkspaceId(1),
|
||||||
location: (["/tmp", "/tmp2"]).into(),
|
location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
|
||||||
center_group: Default::default(),
|
center_group: Default::default(),
|
||||||
bounds: Default::default(),
|
bounds: Default::default(),
|
||||||
display: Default::default(),
|
display: Default::default(),
|
||||||
@ -785,7 +920,7 @@ mod tests {
|
|||||||
|
|
||||||
let workspace_2 = SerializedWorkspace {
|
let workspace_2 = SerializedWorkspace {
|
||||||
id: WorkspaceId(2),
|
id: WorkspaceId(2),
|
||||||
location: (["/tmp"]).into(),
|
location: LocalPaths::new(["/tmp"]).into(),
|
||||||
center_group: Default::default(),
|
center_group: Default::default(),
|
||||||
bounds: Default::default(),
|
bounds: Default::default(),
|
||||||
display: Default::default(),
|
display: Default::default(),
|
||||||
@ -812,7 +947,7 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
workspace_1.location = (["/tmp", "/tmp3"]).into();
|
workspace_1.location = LocalPaths::new(["/tmp", "/tmp3"]).into();
|
||||||
db.save_workspace(workspace_1.clone()).await;
|
db.save_workspace(workspace_1.clone()).await;
|
||||||
db.save_workspace(workspace_1).await;
|
db.save_workspace(workspace_1).await;
|
||||||
db.save_workspace(workspace_2).await;
|
db.save_workspace(workspace_2).await;
|
||||||
@ -885,7 +1020,7 @@ mod tests {
|
|||||||
|
|
||||||
let workspace = SerializedWorkspace {
|
let workspace = SerializedWorkspace {
|
||||||
id: WorkspaceId(5),
|
id: WorkspaceId(5),
|
||||||
location: (["/tmp", "/tmp2"]).into(),
|
location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
|
||||||
center_group,
|
center_group,
|
||||||
bounds: Default::default(),
|
bounds: Default::default(),
|
||||||
display: Default::default(),
|
display: Default::default(),
|
||||||
@ -915,7 +1050,7 @@ mod tests {
|
|||||||
|
|
||||||
let workspace_1 = SerializedWorkspace {
|
let workspace_1 = SerializedWorkspace {
|
||||||
id: WorkspaceId(1),
|
id: WorkspaceId(1),
|
||||||
location: (["/tmp", "/tmp2"]).into(),
|
location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
|
||||||
center_group: Default::default(),
|
center_group: Default::default(),
|
||||||
bounds: Default::default(),
|
bounds: Default::default(),
|
||||||
display: Default::default(),
|
display: Default::default(),
|
||||||
@ -926,7 +1061,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut workspace_2 = SerializedWorkspace {
|
let mut workspace_2 = SerializedWorkspace {
|
||||||
id: WorkspaceId(2),
|
id: WorkspaceId(2),
|
||||||
location: (["/tmp"]).into(),
|
location: LocalPaths::new(["/tmp"]).into(),
|
||||||
center_group: Default::default(),
|
center_group: Default::default(),
|
||||||
bounds: Default::default(),
|
bounds: Default::default(),
|
||||||
display: Default::default(),
|
display: Default::default(),
|
||||||
@ -953,7 +1088,7 @@ mod tests {
|
|||||||
assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
|
assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
|
||||||
|
|
||||||
// Test 'mutate' case of updating a pre-existing id
|
// Test 'mutate' case of updating a pre-existing id
|
||||||
workspace_2.location = (["/tmp", "/tmp2"]).into();
|
workspace_2.location = LocalPaths::new(["/tmp", "/tmp2"]).into();
|
||||||
|
|
||||||
db.save_workspace(workspace_2.clone()).await;
|
db.save_workspace(workspace_2.clone()).await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -964,7 +1099,7 @@ mod tests {
|
|||||||
// Test other mechanism for mutating
|
// Test other mechanism for mutating
|
||||||
let mut workspace_3 = SerializedWorkspace {
|
let mut workspace_3 = SerializedWorkspace {
|
||||||
id: WorkspaceId(3),
|
id: WorkspaceId(3),
|
||||||
location: (&["/tmp", "/tmp2"]).into(),
|
location: LocalPaths::new(&["/tmp", "/tmp2"]).into(),
|
||||||
center_group: Default::default(),
|
center_group: Default::default(),
|
||||||
bounds: Default::default(),
|
bounds: Default::default(),
|
||||||
display: Default::default(),
|
display: Default::default(),
|
||||||
@ -980,7 +1115,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Make sure that updating paths differently also works
|
// Make sure that updating paths differently also works
|
||||||
workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
|
workspace_3.location = LocalPaths::new(["/tmp3", "/tmp4", "/tmp2"]).into();
|
||||||
db.save_workspace(workspace_3.clone()).await;
|
db.save_workspace(workspace_3.clone()).await;
|
||||||
assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
|
assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -999,7 +1134,7 @@ mod tests {
|
|||||||
) -> SerializedWorkspace {
|
) -> SerializedWorkspace {
|
||||||
SerializedWorkspace {
|
SerializedWorkspace {
|
||||||
id: WorkspaceId(4),
|
id: WorkspaceId(4),
|
||||||
location: workspace_id.into(),
|
location: LocalPaths::new(workspace_id).into(),
|
||||||
center_group: center_group.clone(),
|
center_group: center_group.clone(),
|
||||||
bounds: Default::default(),
|
bounds: Default::default(),
|
||||||
display: Default::default(),
|
display: Default::default(),
|
||||||
|
@ -2,12 +2,14 @@ use super::SerializedAxis;
|
|||||||
use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
|
use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
|
use client::RemoteProjectId;
|
||||||
use db::sqlez::{
|
use db::sqlez::{
|
||||||
bindable::{Bind, Column, StaticColumnCount},
|
bindable::{Bind, Column, StaticColumnCount},
|
||||||
statement::Statement,
|
statement::Statement,
|
||||||
};
|
};
|
||||||
use gpui::{AsyncWindowContext, Bounds, DevicePixels, Model, Task, View, WeakView};
|
use gpui::{AsyncWindowContext, Bounds, DevicePixels, Model, Task, View, WeakView};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@ -15,59 +17,98 @@ use std::{
|
|||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
pub struct WorkspaceLocation(Arc<Vec<PathBuf>>);
|
pub struct SerializedRemoteProject {
|
||||||
|
pub id: RemoteProjectId,
|
||||||
|
pub dev_server_name: String,
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub struct LocalPaths(Arc<Vec<PathBuf>>);
|
||||||
|
|
||||||
|
impl LocalPaths {
|
||||||
|
pub fn new<P: AsRef<Path>>(paths: impl IntoIterator<Item = P>) -> Self {
|
||||||
|
let mut paths: Vec<PathBuf> = paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| p.as_ref().to_path_buf())
|
||||||
|
.collect();
|
||||||
|
paths.sort();
|
||||||
|
Self(Arc::new(paths))
|
||||||
|
}
|
||||||
|
|
||||||
impl WorkspaceLocation {
|
|
||||||
pub fn paths(&self) -> Arc<Vec<PathBuf>> {
|
pub fn paths(&self) -> Arc<Vec<PathBuf>> {
|
||||||
self.0.clone()
|
self.0.clone()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
impl From<LocalPaths> for SerializedWorkspaceLocation {
|
||||||
pub fn new<P: AsRef<Path>>(paths: Vec<P>) -> Self {
|
fn from(local_paths: LocalPaths) -> Self {
|
||||||
Self(Arc::new(
|
Self::Local(local_paths)
|
||||||
paths
|
|
||||||
.into_iter()
|
|
||||||
.map(|p| p.as_ref().to_path_buf())
|
|
||||||
.collect(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {
|
impl StaticColumnCount for LocalPaths {}
|
||||||
fn from(iterator: T) -> Self {
|
impl Bind for &LocalPaths {
|
||||||
let mut roots = iterator
|
|
||||||
.into_iter()
|
|
||||||
.map(|p| p.as_ref().to_path_buf())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
roots.sort();
|
|
||||||
Self(Arc::new(roots))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StaticColumnCount for WorkspaceLocation {}
|
|
||||||
impl Bind for &WorkspaceLocation {
|
|
||||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||||
bincode::serialize(&self.0)
|
statement.bind(&bincode::serialize(&self.0)?, start_index)
|
||||||
.expect("Bincode serialization of paths should not fail")
|
|
||||||
.bind(statement, start_index)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Column for WorkspaceLocation {
|
impl Column for LocalPaths {
|
||||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||||
let blob = statement.column_blob(start_index)?;
|
let path_blob = statement.column_blob(start_index)?;
|
||||||
|
let paths: Arc<Vec<PathBuf>> = if path_blob.is_empty() {
|
||||||
|
Default::default()
|
||||||
|
} else {
|
||||||
|
bincode::deserialize(path_blob).context("Bincode deserialization of paths failed")?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((Self(paths), start_index + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SerializedRemoteProject> for SerializedWorkspaceLocation {
|
||||||
|
fn from(remote_project: SerializedRemoteProject) -> Self {
|
||||||
|
Self::Remote(remote_project)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StaticColumnCount for SerializedRemoteProject {}
|
||||||
|
impl Bind for &SerializedRemoteProject {
|
||||||
|
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||||
|
let next_index = statement.bind(&self.id.0, start_index)?;
|
||||||
|
let next_index = statement.bind(&self.dev_server_name, next_index)?;
|
||||||
|
statement.bind(&self.path, next_index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Column for SerializedRemoteProject {
|
||||||
|
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||||
|
let id = statement.column_int64(start_index)?;
|
||||||
|
let dev_server_name = statement.column_text(start_index + 1)?.to_string();
|
||||||
|
let path = statement.column_text(start_index + 2)?.to_string();
|
||||||
Ok((
|
Ok((
|
||||||
WorkspaceLocation(bincode::deserialize(blob).context("Bincode failed")?),
|
Self {
|
||||||
start_index + 1,
|
id: RemoteProjectId(id as u64),
|
||||||
|
dev_server_name,
|
||||||
|
path,
|
||||||
|
},
|
||||||
|
start_index + 3,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub enum SerializedWorkspaceLocation {
|
||||||
|
Local(LocalPaths),
|
||||||
|
Remote(SerializedRemoteProject),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub(crate) struct SerializedWorkspace {
|
pub(crate) struct SerializedWorkspace {
|
||||||
pub(crate) id: WorkspaceId,
|
pub(crate) id: WorkspaceId,
|
||||||
pub(crate) location: WorkspaceLocation,
|
pub(crate) location: SerializedWorkspaceLocation,
|
||||||
pub(crate) center_group: SerializedPaneGroup,
|
pub(crate) center_group: SerializedPaneGroup,
|
||||||
pub(crate) bounds: Option<Bounds<DevicePixels>>,
|
pub(crate) bounds: Option<Bounds<DevicePixels>>,
|
||||||
pub(crate) fullscreen: bool,
|
pub(crate) fullscreen: bool,
|
||||||
|
@ -46,7 +46,7 @@ pub use pane::*;
|
|||||||
pub use pane_group::*;
|
pub use pane_group::*;
|
||||||
use persistence::{model::SerializedWorkspace, SerializedWindowsBounds, DB};
|
use persistence::{model::SerializedWorkspace, SerializedWindowsBounds, DB};
|
||||||
pub use persistence::{
|
pub use persistence::{
|
||||||
model::{ItemId, WorkspaceLocation},
|
model::{ItemId, LocalPaths, SerializedRemoteProject, SerializedWorkspaceLocation},
|
||||||
WorkspaceDb, DB as WORKSPACE_DB,
|
WorkspaceDb, DB as WORKSPACE_DB,
|
||||||
};
|
};
|
||||||
use postage::stream::Stream;
|
use postage::stream::Stream;
|
||||||
@ -82,7 +82,7 @@ use ui::{
|
|||||||
InteractiveElement as _, IntoElement, Label, ParentElement as _, Pixels, SharedString,
|
InteractiveElement as _, IntoElement, Label, ParentElement as _, Pixels, SharedString,
|
||||||
Styled as _, ViewContext, VisualContext as _, WindowContext,
|
Styled as _, ViewContext, VisualContext as _, WindowContext,
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
use util::{maybe, ResultExt};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
pub use workspace_settings::{
|
pub use workspace_settings::{
|
||||||
AutosaveSetting, RestoreOnStartupBehaviour, TabBarSettings, WorkspaceSettings,
|
AutosaveSetting, RestoreOnStartupBehaviour, TabBarSettings, WorkspaceSettings,
|
||||||
@ -3392,17 +3392,16 @@ impl Workspace {
|
|||||||
self.database_id
|
self.database_id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
|
fn local_paths(&self, cx: &AppContext) -> Option<LocalPaths> {
|
||||||
let project = self.project().read(cx);
|
let project = self.project().read(cx);
|
||||||
|
|
||||||
if project.is_local() {
|
if project.is_local() {
|
||||||
Some(
|
Some(LocalPaths::new(
|
||||||
project
|
project
|
||||||
.visible_worktrees(cx)
|
.visible_worktrees(cx)
|
||||||
.map(|worktree| worktree.read(cx).abs_path())
|
.map(|worktree| worktree.read(cx).abs_path())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>(),
|
||||||
.into(),
|
))
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@ -3540,11 +3539,31 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(location) = self.location(cx) {
|
let location = if let Some(local_paths) = self.local_paths(cx) {
|
||||||
// Load bearing special case:
|
if !local_paths.paths().is_empty() {
|
||||||
// - with_local_workspace() relies on this to not have other stuff open
|
Some(SerializedWorkspaceLocation::Local(local_paths))
|
||||||
// when you open your log
|
} else {
|
||||||
if !location.paths().is_empty() {
|
None
|
||||||
|
}
|
||||||
|
} else if let Some(remote_project_id) = self.project().read(cx).remote_project_id() {
|
||||||
|
let store = remote_projects::Store::global(cx).read(cx);
|
||||||
|
maybe!({
|
||||||
|
let project = store.remote_project(remote_project_id)?;
|
||||||
|
let dev_server = store.dev_server(project.dev_server_id)?;
|
||||||
|
|
||||||
|
let remote_project = SerializedRemoteProject {
|
||||||
|
id: remote_project_id,
|
||||||
|
dev_server_name: dev_server.name.to_string(),
|
||||||
|
path: project.path.to_string(),
|
||||||
|
};
|
||||||
|
Some(SerializedWorkspaceLocation::Remote(remote_project))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// don't save workspace state for the empty workspace.
|
||||||
|
if let Some(location) = location {
|
||||||
let center_group = build_serialized_pane_group(&self.center.root, cx);
|
let center_group = build_serialized_pane_group(&self.center.root, cx);
|
||||||
let docks = build_serialized_docks(self, cx);
|
let docks = build_serialized_docks(self, cx);
|
||||||
let serialized_workspace = SerializedWorkspace {
|
let serialized_workspace = SerializedWorkspace {
|
||||||
@ -3559,7 +3578,6 @@ impl Workspace {
|
|||||||
};
|
};
|
||||||
return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
|
return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Task::ready(())
|
Task::ready(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4303,7 +4321,7 @@ pub fn activate_workspace_for_project(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
|
pub async fn last_opened_workspace_paths() -> Option<LocalPaths> {
|
||||||
DB.last_workspace().await.log_err().flatten()
|
DB.last_workspace().await.log_err().flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4410,7 +4428,6 @@ async fn join_channel_internal(
|
|||||||
if let Some((project, host)) = room.most_active_project(cx) {
|
if let Some((project, host)) = room.most_active_project(cx) {
|
||||||
return Some(join_in_room_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
|
||||||
if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
|
if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
|
||||||
if let Some(workspace) = requesting_window {
|
if let Some(workspace) = requesting_window {
|
||||||
@ -4419,7 +4436,7 @@ async fn join_channel_internal(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let project = workspace.project.read(cx);
|
let project = workspace.project.read(cx);
|
||||||
if project.is_local()
|
if (project.is_local() || project.remote_project_id().is_some())
|
||||||
&& project.visible_worktrees(cx).any(|tree| {
|
&& project.visible_worktrees(cx).any(|tree| {
|
||||||
tree.read(cx)
|
tree.read(cx)
|
||||||
.root_entry()
|
.root_entry()
|
||||||
|
@ -71,6 +71,7 @@ project_panel.workspace = true
|
|||||||
project_symbols.workspace = true
|
project_symbols.workspace = true
|
||||||
quick_action_bar.workspace = true
|
quick_action_bar.workspace = true
|
||||||
recent_projects.workspace = true
|
recent_projects.workspace = true
|
||||||
|
remote_projects.workspace = true
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
rope.workspace = true
|
rope.workspace = true
|
||||||
search.workspace = true
|
search.workspace = true
|
||||||
|
@ -286,6 +286,7 @@ fn init_ui(args: Args) {
|
|||||||
ThemeRegistry::global(cx),
|
ThemeRegistry::global(cx),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
remote_projects::init(client.clone(), cx);
|
||||||
|
|
||||||
load_user_themes_in_background(fs.clone(), cx);
|
load_user_themes_in_background(fs.clone(), cx);
|
||||||
watch_themes(fs.clone(), cx);
|
watch_themes(fs.clone(), cx);
|
||||||
|
@ -42,6 +42,7 @@ let instanceCount = 1;
|
|||||||
let isReleaseMode = false;
|
let isReleaseMode = false;
|
||||||
let isTop = false;
|
let isTop = false;
|
||||||
let othersOnStable = false;
|
let othersOnStable = false;
|
||||||
|
let isStateful = false;
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
while (args.length > 0) {
|
while (args.length > 0) {
|
||||||
@ -52,6 +53,8 @@ while (args.length > 0) {
|
|||||||
instanceCount = parseInt(digitMatch[1]);
|
instanceCount = parseInt(digitMatch[1]);
|
||||||
} else if (arg === "--release") {
|
} else if (arg === "--release") {
|
||||||
isReleaseMode = true;
|
isReleaseMode = true;
|
||||||
|
} else if (arg == "--stateful") {
|
||||||
|
isStateful = true;
|
||||||
} else if (arg === "--top") {
|
} else if (arg === "--top") {
|
||||||
isTop = true;
|
isTop = true;
|
||||||
} else if (arg === "--help") {
|
} else if (arg === "--help") {
|
||||||
@ -147,7 +150,7 @@ setTimeout(() => {
|
|||||||
env: {
|
env: {
|
||||||
ZED_IMPERSONATE: users[i],
|
ZED_IMPERSONATE: users[i],
|
||||||
ZED_WINDOW_POSITION: position,
|
ZED_WINDOW_POSITION: position,
|
||||||
ZED_STATELESS: "1",
|
ZED_STATELESS: isStateful && i == 0 ? "1" : "",
|
||||||
ZED_ALWAYS_ACTIVE: "1",
|
ZED_ALWAYS_ACTIVE: "1",
|
||||||
ZED_SERVER_URL: "http://localhost:3000",
|
ZED_SERVER_URL: "http://localhost:3000",
|
||||||
ZED_RPC_URL: "http://localhost:8080/rpc",
|
ZED_RPC_URL: "http://localhost:8080/rpc",
|
||||||
|
Loading…
Reference in New Issue
Block a user