mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-20 19:08:00 +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",
|
||||
"rand 0.8.5",
|
||||
"release_channel",
|
||||
"remote_projects",
|
||||
"reqwest",
|
||||
"rpc",
|
||||
"rustc-demangle",
|
||||
@ -2299,7 +2300,6 @@ dependencies = [
|
||||
"editor",
|
||||
"emojis",
|
||||
"extensions_ui",
|
||||
"feature_flags",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
@ -7728,7 +7728,9 @@ dependencies = [
|
||||
name = "recent_projects"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
@ -7736,10 +7738,15 @@ dependencies = [
|
||||
"ordered-float 2.10.0",
|
||||
"picker",
|
||||
"project",
|
||||
"remote_projects",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"theme",
|
||||
"ui",
|
||||
"ui_text_field",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
@ -7866,6 +7873,18 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "remote_projects"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"gpui",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rend"
|
||||
version = "0.4.0"
|
||||
@ -12303,6 +12322,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"project",
|
||||
"remote_projects",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -12601,6 +12621,7 @@ dependencies = [
|
||||
"quick_action_bar",
|
||||
"recent_projects",
|
||||
"release_channel",
|
||||
"remote_projects",
|
||||
"rope",
|
||||
"search",
|
||||
"serde",
|
||||
|
@ -67,6 +67,7 @@ members = [
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
"crates/release_channel",
|
||||
"crates/remote_projects",
|
||||
"crates/rich_text",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
@ -200,6 +201,7 @@ project_symbols = { path = "crates/project_symbols" }
|
||||
quick_action_bar = { path = "crates/quick_action_bar" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
remote_projects = { path = "crates/remote_projects" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rope = { path = "crates/rope" }
|
||||
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">
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
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>
|
||||
|
Before Width: | Height: | Size: 692 B After Width: | Height: | Size: 413 B |
@ -1203,14 +1203,24 @@ impl Room {
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> 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() {
|
||||
return Task::ready(Ok(project_id));
|
||||
}
|
||||
|
||||
let request = self.client.request(proto::ShareProject {
|
||||
self.client.request(proto::ShareProject {
|
||||
room_id: self.id(),
|
||||
worktrees: project.read(cx).worktree_metadata_protos(cx),
|
||||
});
|
||||
remote_project_id: None,
|
||||
})
|
||||
};
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
|
||||
|
@ -11,9 +11,7 @@ pub use channel_chat::{
|
||||
mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId,
|
||||
MessageParams,
|
||||
};
|
||||
pub use channel_store::{
|
||||
Channel, ChannelEvent, ChannelMembership, ChannelStore, DevServer, RemoteProject,
|
||||
};
|
||||
pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore};
|
||||
|
||||
#[cfg(test)]
|
||||
mod channel_store_tests;
|
||||
|
@ -3,10 +3,7 @@ mod channel_index;
|
||||
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
|
||||
use anyhow::{anyhow, Result};
|
||||
use channel_index::ChannelIndex;
|
||||
use client::{
|
||||
ChannelId, Client, ClientSettings, DevServerId, ProjectId, RemoteProjectId, Subscription, User,
|
||||
UserId, UserStore,
|
||||
};
|
||||
use client::{ChannelId, Client, ClientSettings, ProjectId, Subscription, User, UserId, UserStore};
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
@ -15,7 +12,7 @@ use gpui::{
|
||||
};
|
||||
use language::Capability;
|
||||
use rpc::{
|
||||
proto::{self, ChannelRole, ChannelVisibility, DevServerStatus},
|
||||
proto::{self, ChannelRole, ChannelVisibility},
|
||||
TypedEnvelope,
|
||||
};
|
||||
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 channel_index: ChannelIndex,
|
||||
channel_invitations: Vec<Arc<Channel>>,
|
||||
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
||||
channel_states: HashMap<ChannelId, ChannelState>,
|
||||
hosted_projects: HashMap<ProjectId, HostedProject>,
|
||||
remote_projects: HashMap<RemoteProjectId, RemoteProject>,
|
||||
dev_servers: HashMap<DevServerId, DevServer>,
|
||||
|
||||
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
||||
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
||||
@ -133,8 +85,6 @@ pub struct ChannelState {
|
||||
observed_chat_message: Option<u64>,
|
||||
role: Option<ChannelRole>,
|
||||
projects: HashSet<ProjectId>,
|
||||
dev_servers: HashSet<DevServerId>,
|
||||
remote_projects: HashSet<RemoteProjectId>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
@ -265,8 +215,6 @@ impl ChannelStore {
|
||||
channel_index: ChannelIndex::default(),
|
||||
channel_participants: Default::default(),
|
||||
hosted_projects: Default::default(),
|
||||
remote_projects: Default::default(),
|
||||
dev_servers: Default::default(),
|
||||
outgoing_invites: Default::default(),
|
||||
opened_buffers: Default::default(),
|
||||
opened_chats: Default::default(),
|
||||
@ -366,40 +314,6 @@ impl ChannelStore {
|
||||
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 {
|
||||
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
|
||||
if let OpenedModelHandle::Open(buffer) = buffer {
|
||||
@ -901,46 +815,6 @@ impl ChannelStore {
|
||||
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(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
@ -1221,11 +1095,7 @@ impl ChannelStore {
|
||||
|| !payload.latest_channel_message_ids.is_empty()
|
||||
|| !payload.latest_channel_buffer_versions.is_empty()
|
||||
|| !payload.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();
|
||||
|| !payload.deleted_hosted_projects.is_empty();
|
||||
|
||||
if channels_changed {
|
||||
if !payload.delete_channels.is_empty() {
|
||||
@ -1313,60 +1183,6 @@ impl ChannelStore {
|
||||
.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();
|
||||
@ -1481,20 +1297,4 @@ impl ChannelState {
|
||||
fn remove_hosted_project(&mut self, project_id: ProjectId) {
|
||||
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)]
|
||||
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);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
@ -93,6 +93,7 @@ notifications = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
release_channel.workspace = true
|
||||
remote_projects.workspace = true
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
|
||||
serde_json.workspace = true
|
||||
|
@ -398,26 +398,21 @@ CREATE TABLE hosted_projects (
|
||||
channel_id INTEGER NOT NULL REFERENCES channels(id),
|
||||
name TEXT NOT NULL,
|
||||
visibility TEXT NOT NULL,
|
||||
deleted_at TIMESTAMP NULL,
|
||||
dev_server_id INTEGER REFERENCES dev_servers(id),
|
||||
dev_server_path TEXT
|
||||
deleted_at TIMESTAMP NULL
|
||||
);
|
||||
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 TABLE dev_servers (
|
||||
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,
|
||||
hashed_token TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX idx_dev_servers_on_channel_id ON dev_servers (channel_id);
|
||||
|
||||
CREATE TABLE remote_projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
channel_id INTEGER NOT NULL REFERENCES channels(id),
|
||||
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
|
||||
name 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_participants: HashMap<ChannelId, Vec<UserId>>,
|
||||
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_channel_messages: Vec<proto::ChannelMessageId>,
|
||||
@ -764,6 +762,7 @@ pub struct Project {
|
||||
pub collaborators: Vec<ProjectCollaborator>,
|
||||
pub worktrees: BTreeMap<u64, Worktree>,
|
||||
pub language_servers: Vec<proto::LanguageServer>,
|
||||
pub remote_project_id: Option<RemoteProjectId>,
|
||||
}
|
||||
|
||||
pub struct ProjectCollaborator {
|
||||
@ -786,8 +785,7 @@ impl ProjectCollaborator {
|
||||
#[derive(Debug)]
|
||||
pub struct LeftProject {
|
||||
pub id: ProjectId,
|
||||
pub host_user_id: Option<UserId>,
|
||||
pub host_connection_id: Option<ConnectionId>,
|
||||
pub should_unshare: bool,
|
||||
pub connection_ids: Vec<ConnectionId>,
|
||||
}
|
||||
|
||||
|
@ -640,15 +640,10 @@ impl Database {
|
||||
.get_hosted_projects(&channel_ids, &roles_by_channel_id, tx)
|
||||
.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 {
|
||||
channel_memberships,
|
||||
channels,
|
||||
hosted_projects,
|
||||
dev_servers,
|
||||
remote_projects,
|
||||
channel_participants,
|
||||
latest_buffer_versions,
|
||||
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 {
|
||||
pub async fn get_dev_server(
|
||||
@ -16,40 +19,105 @@ impl Database {
|
||||
.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,
|
||||
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,
|
||||
) -> crate::Result<Vec<dev_server::Model>> {
|
||||
let servers = dev_server::Entity::find()
|
||||
.filter(dev_server::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
|
||||
) -> crate::Result<proto::RemoteProjectsUpdate> {
|
||||
let dev_servers = dev_server::Entity::find()
|
||||
.filter(dev_server::Column::UserId.eq(user_id))
|
||||
.all(tx)
|
||||
.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(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
name: &str,
|
||||
hashed_access_token: &str,
|
||||
user_id: UserId,
|
||||
) -> crate::Result<(channel::Model, dev_server::Model)> {
|
||||
) -> crate::Result<(dev_server::Model, proto::RemoteProjectsUpdate)> {
|
||||
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 {
|
||||
id: ActiveValue::NotSet,
|
||||
hashed_token: ActiveValue::Set(hashed_access_token.to_string()),
|
||||
channel_id: ActiveValue::Set(channel_id),
|
||||
name: ActiveValue::Set(name.to_string()),
|
||||
user_id: ActiveValue::Set(user_id),
|
||||
})
|
||||
.exec_with_returning(&*tx)
|
||||
.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
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ impl Database {
|
||||
room_id: RoomId,
|
||||
connection: ConnectionId,
|
||||
worktrees: &[proto::WorktreeMetadata],
|
||||
remote_project_id: Option<RemoteProjectId>,
|
||||
) -> Result<TransactionGuard<(ProjectId, proto::Room)>> {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let participant = room_participant::Entity::find()
|
||||
@ -58,6 +59,30 @@ impl Database {
|
||||
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 {
|
||||
room_id: ActiveValue::set(Some(participant.room_id)),
|
||||
host_user_id: ActiveValue::set(Some(participant.user_id)),
|
||||
@ -111,6 +136,7 @@ impl Database {
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
connection: ConnectionId,
|
||||
user_id: Option<UserId>,
|
||||
) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
|
||||
self.project_transaction(project_id, |tx| async move {
|
||||
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
@ -118,19 +144,37 @@ impl Database {
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("project not found"))?;
|
||||
if project.host_connection()? == connection {
|
||||
let room = if let Some(room_id) = project.room_id {
|
||||
Some(self.get_room(room_id, &tx).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if project.host_connection()? == connection {
|
||||
project::Entity::delete(project.into_active_model())
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
Ok((room, guest_connection_ids))
|
||||
} else {
|
||||
return Ok((room, guest_connection_ids));
|
||||
}
|
||||
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"))?
|
||||
}
|
||||
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
|
||||
}
|
||||
@ -753,6 +797,7 @@ impl Database {
|
||||
name: language_server.name,
|
||||
})
|
||||
.collect(),
|
||||
remote_project_id: project.remote_project_id,
|
||||
};
|
||||
Ok((project, replica_id as ReplicaId))
|
||||
}
|
||||
@ -794,8 +839,7 @@ impl Database {
|
||||
Ok(LeftProject {
|
||||
id: project.id,
|
||||
connection_ids,
|
||||
host_user_id: None,
|
||||
host_connection_id: None,
|
||||
should_unshare: false,
|
||||
})
|
||||
})
|
||||
.await
|
||||
@ -832,7 +876,7 @@ impl Database {
|
||||
.find_related(project_collaborator::Entity)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
let connection_ids = collaborators
|
||||
let connection_ids: Vec<ConnectionId> = collaborators
|
||||
.into_iter()
|
||||
.map(|collaborator| collaborator.connection())
|
||||
.collect();
|
||||
@ -870,8 +914,7 @@ impl Database {
|
||||
|
||||
let left_project = LeftProject {
|
||||
id: project_id,
|
||||
host_user_id: project.host_user_id,
|
||||
host_connection_id: Some(project.host_connection()?),
|
||||
should_unshare: connection == project.host_connection()?,
|
||||
connection_ids,
|
||||
};
|
||||
Ok((room, left_project))
|
||||
@ -914,7 +957,7 @@ impl Database {
|
||||
capability: Capability,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> 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)
|
||||
.one(tx)
|
||||
.await?
|
||||
@ -933,27 +976,44 @@ impl Database {
|
||||
PrincipalId::UserId(user_id) => user_id,
|
||||
};
|
||||
|
||||
let role = if let Some(remote_project) = remote_project {
|
||||
let channel = channel::Entity::find_by_id(remote_project.channel_id)
|
||||
.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()
|
||||
let role_from_room = if let Some(room_id) = project.room_id {
|
||||
room_participant::Entity::find()
|
||||
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
|
||||
.one(tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such room"))?;
|
||||
|
||||
current_participant.role.unwrap_or(ChannelRole::Guest)
|
||||
.and_then(|participant| participant.role)
|
||||
} 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 {
|
||||
Capability::ReadWrite => {
|
||||
|
@ -8,8 +8,8 @@ use sea_orm::{
|
||||
use crate::db::ProjectId;
|
||||
|
||||
use super::{
|
||||
channel, project, project_collaborator, remote_project, worktree, ChannelId, Database,
|
||||
DevServerId, RejoinedProject, RemoteProjectId, ResharedProject, ServerId, UserId,
|
||||
dev_server, project, project_collaborator, remote_project, worktree, Database, DevServerId,
|
||||
RejoinedProject, RemoteProjectId, ResharedProject, ServerId, UserId,
|
||||
};
|
||||
|
||||
impl Database {
|
||||
@ -26,29 +26,6 @@ impl Database {
|
||||
.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(
|
||||
&self,
|
||||
dev_server_id: DevServerId,
|
||||
@ -64,8 +41,6 @@ impl Database {
|
||||
.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,
|
||||
})
|
||||
@ -74,6 +49,38 @@ impl Database {
|
||||
.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(
|
||||
&self,
|
||||
connection: ConnectionId,
|
||||
@ -95,28 +102,30 @@ impl Database {
|
||||
|
||||
pub async fn create_remote_project(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
dev_server_id: DevServerId,
|
||||
name: &str,
|
||||
path: &str,
|
||||
user_id: UserId,
|
||||
) -> crate::Result<(channel::Model, remote_project::Model)> {
|
||||
) -> crate::Result<(remote_project::Model, proto::RemoteProjectsUpdate)> {
|
||||
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::find_by_id(dev_server_id)
|
||||
.one(&*tx)
|
||||
.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 {
|
||||
name: ActiveValue::Set(name.to_string()),
|
||||
id: ActiveValue::NotSet,
|
||||
channel_id: ActiveValue::Set(channel_id),
|
||||
dev_server_id: ActiveValue::Set(dev_server_id),
|
||||
path: ActiveValue::Set(path.to_string()),
|
||||
})
|
||||
.exec_with_returning(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok((channel, project))
|
||||
let status = self.remote_projects_update_internal(user_id, &tx).await?;
|
||||
|
||||
Ok((project, status))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -127,8 +136,13 @@ impl Database {
|
||||
dev_server_id: DevServerId,
|
||||
connection: ConnectionId,
|
||||
worktrees: &[proto::WorktreeMetadata],
|
||||
) -> crate::Result<proto::RemoteProject> {
|
||||
) -> crate::Result<(proto::RemoteProject, UserId, proto::RemoteProjectsUpdate)> {
|
||||
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)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
@ -168,7 +182,15 @@ impl Database {
|
||||
.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
|
||||
}
|
||||
|
@ -849,11 +849,32 @@ impl Database {
|
||||
.into_values::<_, QueryProjectIds>()
|
||||
.all(&*tx)
|
||||
.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 collaborators = project_collaborator::Entity::find()
|
||||
.filter(project_collaborator::Column::ProjectId.is_in(project_ids))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(collaborator) = collaborators.next().await {
|
||||
let collaborator = collaborator?;
|
||||
let left_project =
|
||||
@ -861,9 +882,8 @@ impl Database {
|
||||
.entry(collaborator.project_id)
|
||||
.or_insert(LeftProject {
|
||||
id: collaborator.project_id,
|
||||
host_user_id: Default::default(),
|
||||
connection_ids: Default::default(),
|
||||
host_connection_id: None,
|
||||
should_unshare: false,
|
||||
});
|
||||
|
||||
let collaborator_connection_id = collaborator.connection();
|
||||
@ -871,9 +891,10 @@ impl Database {
|
||||
left_project.connection_ids.push(collaborator_connection_id);
|
||||
}
|
||||
|
||||
if collaborator.is_host {
|
||||
left_project.host_user_id = Some(collaborator.user_id);
|
||||
left_project.host_connection_id = Some(collaborator_connection_id);
|
||||
if (collaborator.is_host && collaborator.connection() == connection)
|
||||
|| remote_projects_to_unshare.contains(&collaborator.project_id)
|
||||
{
|
||||
left_project.should_unshare = true;
|
||||
}
|
||||
}
|
||||
drop(collaborators);
|
||||
@ -915,6 +936,17 @@ impl Database {
|
||||
.exec(&*tx)
|
||||
.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 deleted = if room.participants.is_empty() {
|
||||
let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?;
|
||||
@ -1264,38 +1296,46 @@ impl Database {
|
||||
}
|
||||
drop(db_participants);
|
||||
|
||||
let mut db_projects = db_room
|
||||
let db_projects = db_room
|
||||
.find_related(project::Entity)
|
||||
.find_with_related(worktree::Entity)
|
||||
.stream(tx)
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
while let Some(row) = db_projects.next().await {
|
||||
let (db_project, db_worktree) = row?;
|
||||
for (db_project, db_worktrees) in db_projects {
|
||||
let host_connection = db_project.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 {
|
||||
id: db_project.id.to_proto(),
|
||||
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 {
|
||||
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 followers = Vec::new();
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::db::{ChannelId, DevServerId};
|
||||
use crate::db::{DevServerId, UserId};
|
||||
use rpc::proto;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
@ -8,20 +8,28 @@ pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: DevServerId,
|
||||
pub name: String,
|
||||
pub channel_id: ChannelId,
|
||||
pub user_id: UserId,
|
||||
pub hashed_token: String,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
#[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 {
|
||||
pub fn to_proto(&self, status: proto::DevServerStatus) -> proto::DevServer {
|
||||
proto::DevServer {
|
||||
dev_server_id: self.id.to_proto(),
|
||||
channel_id: self.channel_id.to_proto(),
|
||||
name: self.name.clone(),
|
||||
status: status as i32,
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use super::project;
|
||||
use crate::db::{ChannelId, DevServerId, RemoteProjectId};
|
||||
use crate::db::{DevServerId, RemoteProjectId};
|
||||
use rpc::proto;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
@ -8,9 +8,7 @@ use sea_orm::entity::prelude::*;
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: RemoteProjectId,
|
||||
pub channel_id: ChannelId,
|
||||
pub dev_server_id: DevServerId,
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
@ -20,6 +18,12 @@ impl ActiveModelBehavior for ActiveModel {}
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_one = "super::project::Entity")]
|
||||
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 {
|
||||
@ -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 {
|
||||
pub fn to_proto(&self, project: Option<project::Model>) -> proto::RemoteProject {
|
||||
proto::RemoteProject {
|
||||
id: self.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(),
|
||||
name: self.name.clone(),
|
||||
path: self.path.clone(),
|
||||
}
|
||||
}
|
||||
|
@ -535,18 +535,18 @@ async fn test_project_count(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
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
|
||||
.unwrap();
|
||||
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
|
||||
.unwrap();
|
||||
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
|
||||
|
||||
// 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
|
||||
.unwrap();
|
||||
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
|
||||
|
@ -255,6 +255,13 @@ impl DevServerSession {
|
||||
pub fn dev_server_id(&self) -> DevServerId {
|
||||
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 {
|
||||
@ -405,6 +412,7 @@ impl Server {
|
||||
.add_request_handler(user_handler(rejoin_remote_projects))
|
||||
.add_request_handler(user_handler(create_remote_project))
|
||||
.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(shutdown_dev_server))
|
||||
.add_request_handler(dev_server_handler(reconnect_dev_server))
|
||||
@ -1044,10 +1052,12 @@ impl Server {
|
||||
.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_channels_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?;
|
||||
|
||||
@ -1067,9 +1077,10 @@ impl Server {
|
||||
)?;
|
||||
self.peer.send(
|
||||
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) =
|
||||
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);
|
||||
}
|
||||
update_dev_server_status(dev_server, proto::DevServerStatus::Online, &session)
|
||||
.await;
|
||||
// todo!() allow only one connection.
|
||||
|
||||
let projects = self
|
||||
.app_state
|
||||
@ -1098,6 +1106,13 @@ impl Server {
|
||||
.await?;
|
||||
self.peer
|
||||
.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?;
|
||||
},
|
||||
Principal::DevServer(dev_server) => {
|
||||
lost_dev_server_connection(&session).await?;
|
||||
update_dev_server_status(&dev_server, proto::DevServerStatus::Offline, &session)
|
||||
.await;
|
||||
Principal::DevServer(_) => {
|
||||
lost_dev_server_connection(&session.for_dev_server().unwrap()).await?;
|
||||
},
|
||||
}
|
||||
},
|
||||
@ -1941,6 +1954,9 @@ async fn share_project(
|
||||
RoomId::from_proto(request.room_id),
|
||||
session.connection_id,
|
||||
&request.worktrees,
|
||||
request
|
||||
.remote_project_id
|
||||
.map(|id| RemoteProjectId::from_proto(id)),
|
||||
)
|
||||
.await?;
|
||||
response.send(proto::ShareProjectResponse {
|
||||
@ -1954,14 +1970,25 @@ async fn share_project(
|
||||
/// Unshare a project from the room.
|
||||
async fn unshare_project(message: proto::UnshareProject, session: Session) -> Result<()> {
|
||||
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
|
||||
.db()
|
||||
.await
|
||||
.unshare_project(project_id, session.connection_id)
|
||||
.unshare_project(project_id, connection_id, user_id)
|
||||
.await?;
|
||||
|
||||
let message = proto::UnshareProject {
|
||||
@ -1969,7 +1996,7 @@ async fn unshare_project_internal(project_id: ProjectId, session: &Session) -> R
|
||||
};
|
||||
|
||||
broadcast(
|
||||
Some(session.connection_id),
|
||||
Some(connection_id),
|
||||
guest_connection_ids.iter().copied(),
|
||||
|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(())
|
||||
}
|
||||
|
||||
/// Share a project into the room.
|
||||
/// DevServer makes a project available online
|
||||
async fn share_remote_project(
|
||||
request: proto::ShareRemoteProject,
|
||||
response: Response<proto::ShareRemoteProject>,
|
||||
session: DevServerSession,
|
||||
) -> Result<()> {
|
||||
let remote_project = session
|
||||
let (remote_project, user_id, status) = session
|
||||
.db()
|
||||
.await
|
||||
.share_remote_project(
|
||||
@ -2000,22 +2027,7 @@ async fn share_remote_project(
|
||||
return Err(anyhow!("failed to share remote project"))?;
|
||||
};
|
||||
|
||||
for (connection_id, _) in session
|
||||
.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();
|
||||
}
|
||||
send_remote_projects_update(user_id, status, &session).await;
|
||||
|
||||
response.send(proto::ShareProjectResponse { project_id })?;
|
||||
|
||||
@ -2081,19 +2093,21 @@ fn join_project_internal(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for collaborator in &collaborators {
|
||||
session
|
||||
.peer
|
||||
.send(
|
||||
collaborator.peer_id.unwrap().into(),
|
||||
proto::AddProjectCollaborator {
|
||||
let add_project_collaborator = proto::AddProjectCollaborator {
|
||||
project_id: project_id.to_proto(),
|
||||
collaborator: Some(proto::Collaborator {
|
||||
peer_id: Some(session.connection_id.into()),
|
||||
replica_id: replica_id.0 as u32,
|
||||
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();
|
||||
}
|
||||
@ -2105,7 +2119,10 @@ fn join_project_internal(
|
||||
replica_id: replica_id.0 as u32,
|
||||
collaborators: collaborators.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) {
|
||||
@ -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?;
|
||||
tracing::info!(
|
||||
%project_id,
|
||||
host_user_id = ?project.host_user_id,
|
||||
host_connection_id = ?project.host_connection_id,
|
||||
"leave project"
|
||||
);
|
||||
|
||||
@ -2224,13 +2239,33 @@ async fn create_remote_project(
|
||||
response: Response<proto::CreateRemoteProject>,
|
||||
session: UserSession,
|
||||
) -> 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()
|
||||
.await
|
||||
.create_remote_project(
|
||||
ChannelId(request.channel_id as i32),
|
||||
DevServerId(request.dev_server_id as i32),
|
||||
&request.name,
|
||||
&request.path,
|
||||
session.user_id(),
|
||||
)
|
||||
@ -2242,25 +2277,12 @@ async fn create_remote_project(
|
||||
.get_remote_projects_for_dev_server(remote_project.dev_server_id)
|
||||
.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(
|
||||
dev_server_connection_id,
|
||||
proto::DevServerInstructions { projects },
|
||||
)?;
|
||||
}
|
||||
|
||||
send_remote_projects_update(session.user_id(), update, &session).await;
|
||||
|
||||
response.send(proto::CreateRemoteProjectResponse {
|
||||
remote_project: Some(remote_project.to_proto(None)),
|
||||
@ -2276,37 +2298,56 @@ async fn create_dev_server(
|
||||
let access_token = auth::random_token();
|
||||
let hashed_access_token = auth::hash_access_token(&access_token);
|
||||
|
||||
let (channel, dev_server) = session
|
||||
let (dev_server, status) = session
|
||||
.db()
|
||||
.await
|
||||
.create_dev_server(
|
||||
ChannelId(request.channel_id as i32),
|
||||
&request.name,
|
||||
&hashed_access_token,
|
||||
session.user_id(),
|
||||
)
|
||||
.create_dev_server(&request.name, &hashed_access_token, session.user_id())
|
||||
.await?;
|
||||
|
||||
let update = proto::UpdateChannels {
|
||||
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())?;
|
||||
}
|
||||
}
|
||||
send_remote_projects_update(session.user_id(), status, &session).await;
|
||||
|
||||
response.send(proto::CreateDevServerResponse {
|
||||
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),
|
||||
name: request.name.clone(),
|
||||
})?;
|
||||
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(
|
||||
request: proto::RejoinRemoteProjects,
|
||||
response: Response<proto::RejoinRemoteProjects>,
|
||||
@ -2403,8 +2444,15 @@ async fn shutdown_dev_server(
|
||||
session: DevServerSession,
|
||||
) -> Result<()> {
|
||||
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 dev_server_id = session.dev_server_id();
|
||||
let db = session.db().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?;
|
||||
@ -2412,22 +2460,26 @@ async fn shutdown_dev_server(
|
||||
};
|
||||
|
||||
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 {
|
||||
remote_projects,
|
||||
dev_servers: vec![dev_server.to_proto(proto::DevServerStatus::Offline)],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for (connection_id, _) in session
|
||||
session
|
||||
.connection_pool()
|
||||
.await
|
||||
.channel_connection_ids(dev_server.channel_id)
|
||||
{
|
||||
session.peer.send(connection_id, update.clone()).trace_err();
|
||||
}
|
||||
.set_dev_server_offline(dev_server_id);
|
||||
|
||||
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(())
|
||||
}
|
||||
@ -4626,7 +4678,7 @@ fn notify_membership_updated(
|
||||
..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
|
||||
.removed_channels
|
||||
.into_iter()
|
||||
@ -4659,7 +4711,6 @@ fn build_update_user_channels(channels: &ChannelsForUser) -> proto::UpdateUserCh
|
||||
fn build_channels_update(
|
||||
channels: ChannelsForUser,
|
||||
channel_invites: Vec<db::Channel>,
|
||||
pool: &ConnectionPool,
|
||||
) -> proto::UpdateChannels {
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
|
||||
@ -4684,13 +4735,6 @@ fn build_channels_update(
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -4777,24 +4821,19 @@ fn channel_updated(
|
||||
);
|
||||
}
|
||||
|
||||
async fn update_dev_server_status(
|
||||
dev_server: &dev_server::Model,
|
||||
status: proto::DevServerStatus,
|
||||
async fn send_remote_projects_update(
|
||||
user_id: UserId,
|
||||
mut status: proto::RemoteProjectsUpdate,
|
||||
session: &Session,
|
||||
) {
|
||||
let pool = session.connection_pool().await;
|
||||
let connections = pool.channel_connection_ids(dev_server.channel_id);
|
||||
for (connection_id, _) in connections {
|
||||
session
|
||||
.peer
|
||||
.send(
|
||||
connection_id,
|
||||
proto::UpdateChannels {
|
||||
dev_servers: vec![dev_server.to_proto(status)],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.trace_err();
|
||||
for dev_server in &mut status.dev_servers {
|
||||
dev_server.status =
|
||||
pool.dev_server_status(DevServerId(dev_server.dev_server_id as i32)) as i32;
|
||||
}
|
||||
let connections = pool.user_connection_ids(user_id);
|
||||
for connection_id in connections {
|
||||
session.peer.send(connection_id, status.clone()).trace_err();
|
||||
}
|
||||
}
|
||||
|
||||
@ -4833,7 +4872,7 @@ async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()>
|
||||
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");
|
||||
let project_ids = session
|
||||
.db()
|
||||
@ -4843,9 +4882,14 @@ async fn lost_dev_server_connection(session: &Session) -> Result<()> {
|
||||
|
||||
for project_id in project_ids {
|
||||
// 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(())
|
||||
}
|
||||
|
||||
@ -4947,7 +4991,7 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
|
||||
|
||||
fn project_left(project: &db::LeftProject, session: &UserSession) {
|
||||
for connection_id in &project.connection_ids {
|
||||
if project.host_user_id == Some(session.user_id()) {
|
||||
if project.should_unshare {
|
||||
session
|
||||
.peer
|
||||
.send(
|
||||
|
@ -13,6 +13,7 @@ pub struct ConnectionPool {
|
||||
connected_users: BTreeMap<UserId, ConnectedPrincipal>,
|
||||
connected_dev_servers: BTreeMap<DevServerId, ConnectionId>,
|
||||
channels: ChannelPool,
|
||||
offline_dev_servers: HashSet<DevServerId>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
@ -106,12 +107,17 @@ impl ConnectionPool {
|
||||
}
|
||||
PrincipalId::DevServerId(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();
|
||||
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> {
|
||||
self.connections.values()
|
||||
}
|
||||
@ -137,7 +143,9 @@ impl ConnectionPool {
|
||||
}
|
||||
|
||||
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
|
||||
} else {
|
||||
proto::DevServerStatus::Offline
|
||||
|
@ -1023,6 +1023,8 @@ async fn test_channel_link_notifications(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
// the new channel shows for b and c
|
||||
assert_channels_list_shape(
|
||||
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 fs::Fs;
|
||||
use gpui::VisualTestContext;
|
||||
use rpc::proto::DevServerStatus;
|
||||
use gpui::{TestAppContext, VisualTestContext, WindowHandle};
|
||||
use rpc::{proto::DevServerStatus, ErrorCode, ErrorExt};
|
||||
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]
|
||||
async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
|
||||
let (server, client) = TestServer::start1(cx).await;
|
||||
|
||||
let channel_id = server
|
||||
.make_channel("test", None, (&client, cx), &mut [])
|
||||
.await;
|
||||
let store = cx.update(|cx| remote_projects::Store::global(cx).clone());
|
||||
|
||||
let resp = client
|
||||
.channel_store()
|
||||
let resp = store
|
||||
.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
|
||||
.unwrap();
|
||||
|
||||
client.channel_store().update(cx, |store, _| {
|
||||
assert_eq!(store.dev_servers_for_id(channel_id).len(), 1);
|
||||
assert_eq!(store.dev_servers_for_id(channel_id)[0].name, "server-1");
|
||||
assert_eq!(
|
||||
store.dev_servers_for_id(channel_id)[0].status,
|
||||
DevServerStatus::Offline
|
||||
);
|
||||
store.update(cx, |store, _| {
|
||||
assert_eq!(store.dev_servers().len(), 1);
|
||||
assert_eq!(store.dev_servers()[0].name, "server-1");
|
||||
assert_eq!(store.dev_servers()[0].status, DevServerStatus::Offline);
|
||||
});
|
||||
|
||||
let dev_server = server.create_dev_server(resp.access_token, cx2).await;
|
||||
cx.executor().run_until_parked();
|
||||
client.channel_store().update(cx, |store, _| {
|
||||
assert_eq!(
|
||||
store.dev_servers_for_id(channel_id)[0].status,
|
||||
DevServerStatus::Online
|
||||
);
|
||||
store.update(cx, |store, _| {
|
||||
assert_eq!(store.dev_servers()[0].status, DevServerStatus::Online);
|
||||
});
|
||||
|
||||
dev_server
|
||||
@ -54,13 +49,10 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
||||
)
|
||||
.await;
|
||||
|
||||
client
|
||||
.channel_store()
|
||||
store
|
||||
.update(cx, |store, cx| {
|
||||
store.create_remote_project(
|
||||
channel_id,
|
||||
client::DevServerId(resp.dev_server_id),
|
||||
"project-1".to_string(),
|
||||
"/remote".to_string(),
|
||||
cx,
|
||||
)
|
||||
@ -70,12 +62,11 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let remote_workspace = client
|
||||
.channel_store()
|
||||
let remote_workspace = store
|
||||
.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[0].name, "project-1");
|
||||
assert_eq!(projects[0].path, "/remote");
|
||||
workspace::join_remote_project(
|
||||
projects[0].project_id.unwrap(),
|
||||
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();
|
||||
|
||||
let cx2 = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut();
|
||||
cx2.simulate_keystrokes("cmd-p 1 enter");
|
||||
let cx = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut();
|
||||
cx.simulate_keystrokes("cmd-p 1 enter");
|
||||
|
||||
let editor = remote_workspace
|
||||
.update(cx2, |ws, cx| {
|
||||
.update(cx, |ws, cx| {
|
||||
ws.active_item_as::<Editor>(cx).unwrap().clone()
|
||||
})
|
||||
.unwrap();
|
||||
editor.update(cx2, |ed, cx| {
|
||||
editor.update(cx, |ed, cx| {
|
||||
assert_eq!(ed.text(cx).to_string(), "remote\nremote\nremote");
|
||||
});
|
||||
cx2.simulate_input("wow!");
|
||||
cx2.simulate_keystrokes("cmd-s");
|
||||
cx.simulate_input("wow!");
|
||||
cx.simulate_keystrokes("cmd-s");
|
||||
|
||||
let content = dev_server
|
||||
.fs()
|
||||
@ -108,3 +99,263 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
||||
.unwrap();
|
||||
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"));
|
||||
|
||||
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.
|
||||
client_b.disconnect(&cx_b.to_async());
|
||||
executor.advance_clock(RECONNECT_TIMEOUT);
|
||||
|
@ -284,6 +284,7 @@ impl TestServer {
|
||||
collab_ui::init(&app_state, cx);
|
||||
file_finder::init(cx);
|
||||
menu::init();
|
||||
remote_projects::init(client.clone(), cx);
|
||||
settings::KeymapFile::load_asset("keymaps/default-macos.json", cx).unwrap();
|
||||
});
|
||||
|
||||
|
@ -39,7 +39,6 @@ db.workspace = true
|
||||
editor.workspace = true
|
||||
emojis.workspace = true
|
||||
extensions_ui.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
|
@ -305,10 +305,6 @@ impl ChannelView {
|
||||
});
|
||||
}
|
||||
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) {
|
||||
self.acknowledge_buffer_version(cx);
|
||||
} else {
|
||||
|
@ -1,20 +1,17 @@
|
||||
mod channel_modal;
|
||||
mod contact_finder;
|
||||
mod dev_server_modal;
|
||||
|
||||
use self::channel_modal::ChannelModal;
|
||||
use self::dev_server_modal::DevServerModal;
|
||||
use crate::{
|
||||
channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
|
||||
CollaborationPanelSettings,
|
||||
};
|
||||
use call::ActiveCall;
|
||||
use channel::{Channel, ChannelEvent, ChannelStore, RemoteProject};
|
||||
use channel::{Channel, ChannelEvent, ChannelStore};
|
||||
use client::{ChannelId, Client, Contact, ProjectId, User, UserStore};
|
||||
use contact_finder::ContactFinder;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
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 project::{Fs, Project};
|
||||
use rpc::{
|
||||
proto::{self, ChannelVisibility, DevServerStatus, PeerId},
|
||||
proto::{self, ChannelVisibility, PeerId},
|
||||
ErrorCode, ErrorExt,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
@ -191,7 +188,6 @@ enum ListEntry {
|
||||
id: ProjectId,
|
||||
name: SharedString,
|
||||
},
|
||||
RemoteProject(channel::RemoteProject),
|
||||
Contact {
|
||||
contact: Arc<Contact>,
|
||||
calling: bool,
|
||||
@ -282,23 +278,10 @@ impl CollabPanel {
|
||||
.push(cx.observe(&this.user_store, |this, _, cx| {
|
||||
this.update_entries(true, cx)
|
||||
}));
|
||||
let mut has_opened = false;
|
||||
this.subscriptions.push(cx.observe(
|
||||
&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.subscriptions
|
||||
.push(cx.observe(&this.channel_store, move |this, _, cx| {
|
||||
this.update_entries(true, cx)
|
||||
},
|
||||
));
|
||||
}));
|
||||
this.subscriptions
|
||||
.push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
|
||||
this.subscriptions.push(cx.subscribe(
|
||||
@ -586,7 +569,6 @@ impl CollabPanel {
|
||||
}
|
||||
|
||||
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
|
||||
.channel_at_index(mat.candidate_id + 1)
|
||||
.map_or(false, |next_channel| {
|
||||
@ -624,12 +606,6 @@ impl CollabPanel {
|
||||
for (name, id) in hosted_projects {
|
||||
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))
|
||||
}
|
||||
|
||||
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 {
|
||||
self.entries.get(ix).map_or(false, |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) {
|
||||
context_menu = context_menu
|
||||
.separator()
|
||||
.entry(
|
||||
context_menu = context_menu.separator().entry(
|
||||
"Manage Members",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| {
|
||||
this.manage_members(channel_id, cx)
|
||||
}),
|
||||
cx.handler_for(&this, move |this, 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 {
|
||||
context_menu = context_menu.entry(
|
||||
"Move this channel",
|
||||
@ -1624,12 +1534,6 @@ impl CollabPanel {
|
||||
} => {
|
||||
// todo()
|
||||
}
|
||||
ListEntry::RemoteProject(project) => {
|
||||
if let Some(project_id) = project.project_id {
|
||||
self.join_remote_project(project_id, cx)
|
||||
}
|
||||
}
|
||||
|
||||
ListEntry::OutgoingRequest(_) => {}
|
||||
ListEntry::ChannelEditor { .. } => {}
|
||||
}
|
||||
@ -1801,18 +1705,6 @@ impl CollabPanel {
|
||||
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>) {
|
||||
if let Some(channel) = self.selected_channel() {
|
||||
self.remove_channel(channel.id, cx)
|
||||
@ -2113,18 +2005,6 @@ impl CollabPanel {
|
||||
.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>) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
@ -2260,9 +2140,6 @@ impl CollabPanel {
|
||||
ListEntry::HostedProject { id, name } => self
|
||||
.render_channel_project(*id, name, is_selected, cx)
|
||||
.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;
|
||||
}
|
||||
}
|
||||
ListEntry::RemoteProject(project) => {
|
||||
if let ListEntry::RemoteProject(other) = other {
|
||||
return project.id == other.id;
|
||||
}
|
||||
}
|
||||
ListEntry::ChannelNotes { channel_id } => {
|
||||
if let ListEntry::ChannelNotes {
|
||||
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 project = self.project.read(cx);
|
||||
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_deafened = room.is_deafened().unwrap_or(false);
|
||||
let is_screen_sharing = room.is_screen_sharing();
|
||||
let can_use_microphone = room.can_use_microphone();
|
||||
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(
|
||||
Button::new(
|
||||
"toggle_sharing",
|
||||
@ -208,7 +211,8 @@ impl Render for CollabTitlebarItem {
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.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 mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
@ -423,15 +427,26 @@ impl CollabTitlebarItem {
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
popover_menu("project_name_trigger")
|
||||
.trigger(
|
||||
Button::new("project_name_trigger", name)
|
||||
.when(!is_project_selected, |b| b.color(Color::Muted))
|
||||
.style(ButtonStyle::Subtle)
|
||||
.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> {
|
||||
@ -607,17 +622,6 @@ impl CollabTitlebarItem {
|
||||
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(
|
||||
&self,
|
||||
status: &client::Status,
|
||||
|
@ -81,6 +81,7 @@ impl FollowableItem for Editor {
|
||||
let mut buffers = futures::future::try_join_all(buffers?)
|
||||
.await
|
||||
.debug_assert_ok("leaders don't share views for unshared buffers")?;
|
||||
|
||||
let editor = pane.update(&mut cx, |pane, cx| {
|
||||
let mut editors = pane.items_of_type::<Self>();
|
||||
editors.find(|editor| {
|
||||
|
@ -1,20 +1,25 @@
|
||||
use anyhow::Result;
|
||||
use client::{user::UserStore, Client, ClientSettings, RemoteProjectId};
|
||||
use client::RemoteProjectId;
|
||||
use client::{user::UserStore, Client, ClientSettings};
|
||||
use fs::Fs;
|
||||
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 node_runtime::NodeRuntime;
|
||||
use postage::stream::Stream;
|
||||
use project::Project;
|
||||
use rpc::{proto, TypedEnvelope};
|
||||
use settings::Settings;
|
||||
use project::{Project, WorktreeSettings};
|
||||
use rpc::{proto, ErrorCode, TypedEnvelope};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub struct DevServer {
|
||||
client: Arc<Client>,
|
||||
app_state: AppState,
|
||||
remote_shutdown: bool,
|
||||
projects: HashMap<RemoteProjectId, Model<Project>>,
|
||||
_subscriptions: Vec<client::Subscription>,
|
||||
_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));
|
||||
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
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
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);
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -89,21 +103,33 @@ impl DevServer {
|
||||
|
||||
DevServer {
|
||||
_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,
|
||||
projects: Default::default(),
|
||||
remote_shutdown: false,
|
||||
app_state,
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if let Some(request) = request {
|
||||
request.await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_dev_server_instructions(
|
||||
this: Model<Self>,
|
||||
@ -148,6 +174,35 @@ impl DevServer {
|
||||
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(
|
||||
&mut self,
|
||||
remote_project_id: &RemoteProjectId,
|
||||
|
@ -11,6 +11,7 @@ pub struct HighlightedText {
|
||||
pub text: String,
|
||||
pub highlight_positions: Vec<usize>,
|
||||
pub char_count: usize,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl HighlightedText {
|
||||
@ -39,13 +40,17 @@ impl HighlightedText {
|
||||
text,
|
||||
highlight_positions,
|
||||
char_count,
|
||||
color: Color::Default,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(self, color: Color) -> Self {
|
||||
Self { color, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for HighlightedText {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
HighlightedLabel::new(self.text, self.highlight_positions)
|
||||
fn render(self, _: &mut WindowContext) -> impl IntoElement {
|
||||
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 async_trait::async_trait;
|
||||
use client::{
|
||||
proto, Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore,
|
||||
proto, Client, Collaborator, PendingEntitySubscription, ProjectId, RemoteProjectId,
|
||||
TypedEnvelope, UserStore,
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
|
||||
@ -207,6 +208,7 @@ pub struct Project {
|
||||
prettier_instances: HashMap<PathBuf, PrettierInstance>,
|
||||
tasks: Model<Inventory>,
|
||||
hosted_project_id: Option<ProjectId>,
|
||||
remote_project_id: Option<client::RemoteProjectId>,
|
||||
search_history: SearchHistory,
|
||||
}
|
||||
|
||||
@ -268,6 +270,7 @@ enum ProjectClientState {
|
||||
capability: Capability,
|
||||
remote_id: u64,
|
||||
replica_id: ReplicaId,
|
||||
in_room: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@ -723,6 +726,7 @@ impl Project {
|
||||
prettier_instances: HashMap::default(),
|
||||
tasks,
|
||||
hosted_project_id: None,
|
||||
remote_project_id: None,
|
||||
search_history: Self::new_search_history(),
|
||||
}
|
||||
})
|
||||
@ -836,6 +840,7 @@ impl Project {
|
||||
capability: Capability::ReadWrite,
|
||||
remote_id,
|
||||
replica_id,
|
||||
in_room: response.payload.remote_project_id.is_none(),
|
||||
},
|
||||
supplementary_language_servers: HashMap::default(),
|
||||
language_servers: Default::default(),
|
||||
@ -877,6 +882,10 @@ impl Project {
|
||||
prettier_instances: HashMap::default(),
|
||||
tasks,
|
||||
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(),
|
||||
};
|
||||
this.set_role(role, cx);
|
||||
@ -1235,6 +1244,10 @@ impl Project {
|
||||
self.hosted_project_id
|
||||
}
|
||||
|
||||
pub fn remote_project_id(&self) -> Option<RemoteProjectId> {
|
||||
self.remote_project_id
|
||||
}
|
||||
|
||||
pub fn replica_id(&self) -> ReplicaId {
|
||||
match self.client_state {
|
||||
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<()> {
|
||||
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"));
|
||||
} else {
|
||||
*in_room = true;
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!("project was already shared"));
|
||||
}
|
||||
}
|
||||
self.client_subscriptions.push(
|
||||
self.client
|
||||
@ -1763,8 +1785,15 @@ impl Project {
|
||||
|
||||
fn unshare_internal(&mut self, cx: &mut AppContext) -> Result<()> {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
if let ProjectClientState::Shared { remote_id, .. } = self.client_state {
|
||||
self.client_state = ProjectClientState::Local;
|
||||
@ -6959,7 +6988,8 @@ impl Project {
|
||||
pub fn is_shared(&self) -> bool {
|
||||
match &self.client_state {
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
menu.workspace = true
|
||||
ordered-float.workspace = true
|
||||
picker.workspace = true
|
||||
remote_projects.workspace = true
|
||||
rpc.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
ui_text_field.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
mod remote_projects;
|
||||
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result,
|
||||
Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||
Subscription, Task, View, ViewContext, WeakView,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
@ -8,11 +11,21 @@ use picker::{
|
||||
highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
|
||||
Picker, PickerDelegate,
|
||||
};
|
||||
use remote_projects::RemoteProjects;
|
||||
use rpc::proto::DevServerStatus;
|
||||
use serde::Deserialize;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use ui::{prelude::*, tooltip_container, ListItem, ListItemSpacing, Tooltip};
|
||||
use util::paths::PathExt;
|
||||
use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
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)]
|
||||
pub struct OpenRecent {
|
||||
@ -25,9 +38,12 @@ fn default_create_new_window() -> bool {
|
||||
}
|
||||
|
||||
gpui::impl_actions!(projects, [OpenRecent]);
|
||||
gpui::actions!(projects, [OpenRemote]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(RecentProjects::register).detach();
|
||||
cx.observe_new_views(remote_projects::RemoteProjects::register)
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub struct RecentProjects {
|
||||
@ -55,10 +71,11 @@ impl RecentProjects {
|
||||
let workspaces = WORKSPACE_DB
|
||||
.recent_workspaces_on_disk()
|
||||
.await
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
this.update(&mut cx, move |this, cx| {
|
||||
this.picker.update(cx, move |picker, cx| {
|
||||
picker.delegate.workspaces = workspaces;
|
||||
picker.delegate.set_workspaces(workspaces);
|
||||
picker.update_matches(picker.query(cx), cx)
|
||||
})
|
||||
})
|
||||
@ -75,9 +92,7 @@ impl RecentProjects {
|
||||
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(|workspace, open_recent: &OpenRecent, cx| {
|
||||
let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
|
||||
if let Some(handler) = Self::open(workspace, open_recent.create_new_window, cx) {
|
||||
handler.detach_and_log_err(cx);
|
||||
}
|
||||
Self::open(workspace, open_recent.create_new_window, cx);
|
||||
return;
|
||||
};
|
||||
|
||||
@ -89,24 +104,17 @@ impl RecentProjects {
|
||||
});
|
||||
}
|
||||
|
||||
fn open(
|
||||
_: &mut Workspace,
|
||||
pub fn open(
|
||||
workspace: &mut Workspace,
|
||||
create_new_window: bool,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
Some(cx.spawn(|workspace, mut cx| async move {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let weak_workspace = cx.view().downgrade();
|
||||
) {
|
||||
let weak = cx.view().downgrade();
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
let delegate =
|
||||
RecentProjectsDelegate::new(weak_workspace, create_new_window, true);
|
||||
|
||||
let delegate = RecentProjectsDelegate::new(weak, create_new_window, true);
|
||||
let modal = Self::new(delegate, 34., cx);
|
||||
modal
|
||||
});
|
||||
})?;
|
||||
Ok(())
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
|
||||
@ -143,13 +151,14 @@ impl Render for RecentProjects {
|
||||
|
||||
pub struct RecentProjectsDelegate {
|
||||
workspace: WeakView<Workspace>,
|
||||
workspaces: Vec<(WorkspaceId, WorkspaceLocation)>,
|
||||
workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>,
|
||||
selected_match_index: usize,
|
||||
matches: Vec<StringMatch>,
|
||||
render_paths: bool,
|
||||
create_new_window: bool,
|
||||
// Flag to reset index when there is a new query vs not reset index when user delete an item
|
||||
reset_selected_match_index: bool,
|
||||
has_any_remote_projects: bool,
|
||||
}
|
||||
|
||||
impl RecentProjectsDelegate {
|
||||
@ -162,8 +171,17 @@ impl RecentProjectsDelegate {
|
||||
create_new_window,
|
||||
render_paths,
|
||||
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 PickerDelegate for RecentProjectsDelegate {
|
||||
@ -210,12 +228,18 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, (_, location))| {
|
||||
let combined_string = location
|
||||
let combined_string = match location {
|
||||
SerializedWorkspaceLocation::Local(paths) => paths
|
||||
.paths()
|
||||
.iter()
|
||||
.map(|path| path.compact().to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
.join(""),
|
||||
SerializedWorkspaceLocation::Remote(remote_project) => {
|
||||
format!("{}{}", remote_project.dev_server_name, remote_project.path)
|
||||
}
|
||||
};
|
||||
|
||||
StringMatchCandidate::new(id, combined_string)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@ -261,7 +285,9 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
if workspace.database_id() == *candidate_workspace_id {
|
||||
Task::ready(Ok(()))
|
||||
} 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 {
|
||||
cx.spawn(move |workspace, mut cx| async move {
|
||||
let continue_replacing = workspace
|
||||
@ -272,11 +298,8 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
if continue_replacing {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.open_workspace_for_paths(
|
||||
true,
|
||||
candidate_paths,
|
||||
cx,
|
||||
)
|
||||
workspace
|
||||
.open_workspace_for_paths(true, paths, cx)
|
||||
})?
|
||||
.await
|
||||
} else {
|
||||
@ -284,7 +307,47 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
}
|
||||
})
|
||||
} 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 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(
|
||||
&self,
|
||||
ix: usize,
|
||||
@ -308,9 +379,30 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
let (workspace_id, location) = &self.workspaces[hit.candidate_id];
|
||||
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 (match_labels, paths): (Vec<_>, Vec<_>) = location
|
||||
.paths()
|
||||
let paths = match location {
|
||||
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()
|
||||
.map(|path| {
|
||||
let path = path.compact();
|
||||
@ -323,22 +415,58 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
.unzip();
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.selected(selected)
|
||||
.inset(true)
|
||||
.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({
|
||||
let mut highlighted = highlighted_match.clone();
|
||||
if !self.render_paths {
|
||||
highlighted.paths.clear();
|
||||
}
|
||||
highlighted.render(cx)
|
||||
})
|
||||
}),
|
||||
)
|
||||
.when(!is_current_workspace, |el| {
|
||||
let delete_button = div()
|
||||
.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
|
||||
@ -406,6 +567,7 @@ fn highlights_for_path(
|
||||
text: text.to_string(),
|
||||
highlight_positions,
|
||||
char_count,
|
||||
color: Color::Default,
|
||||
}
|
||||
});
|
||||
|
||||
@ -415,6 +577,7 @@ fn highlights_for_path(
|
||||
text: path_string.to_string(),
|
||||
highlight_positions: path_positions,
|
||||
char_count: path_char_count,
|
||||
color: Color::Default,
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -430,7 +593,7 @@ impl RecentProjectsDelegate {
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
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.reset_selected_match_index = false;
|
||||
picker.update_matches(picker.query(cx), cx)
|
||||
@ -475,7 +638,7 @@ mod tests {
|
||||
use gpui::{TestAppContext, WindowHandle};
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use workspace::{open_paths, AppState};
|
||||
use workspace::{open_paths, AppState, LocalPaths};
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -539,10 +702,10 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
string: "fake candidate".to_string(),
|
||||
}];
|
||||
delegate.workspaces = vec![(
|
||||
delegate.set_workspaces(vec![(
|
||||
WorkspaceId::default(),
|
||||
WorkspaceLocation::new(vec!["/test/path/"]),
|
||||
)];
|
||||
LocalPaths::new(vec!["/test/path/"]).into(),
|
||||
)]);
|
||||
});
|
||||
})
|
||||
.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;
|
||||
RejoinRemoteProjects rejoin_remote_projects = 186;
|
||||
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;
|
||||
@ -269,6 +273,8 @@ enum ErrorCode {
|
||||
UnsharedItem = 12;
|
||||
NoSuchProject = 13;
|
||||
DevServerAlreadyOnline = 14;
|
||||
DevServerOffline = 15;
|
||||
RemoteProjectPathDoesNotExist = 16;
|
||||
reserved 6;
|
||||
}
|
||||
|
||||
@ -433,6 +439,7 @@ message LiveKitConnectionInfo {
|
||||
message ShareProject {
|
||||
uint64 room_id = 1;
|
||||
repeated WorktreeMetadata worktrees = 2;
|
||||
optional uint64 remote_project_id = 3;
|
||||
}
|
||||
|
||||
message ShareProjectResponse {
|
||||
@ -457,8 +464,8 @@ message JoinHostedProject {
|
||||
}
|
||||
|
||||
message CreateRemoteProject {
|
||||
uint64 channel_id = 1;
|
||||
string name = 2;
|
||||
reserved 1;
|
||||
reserved 2;
|
||||
uint64 dev_server_id = 3;
|
||||
string path = 4;
|
||||
}
|
||||
@ -466,14 +473,18 @@ message CreateRemoteProjectResponse {
|
||||
RemoteProject remote_project = 1;
|
||||
}
|
||||
|
||||
message ValidateRemoteProjectRequest {
|
||||
string path = 1;
|
||||
}
|
||||
|
||||
message CreateDevServer {
|
||||
uint64 channel_id = 1;
|
||||
reserved 1;
|
||||
string name = 2;
|
||||
}
|
||||
|
||||
message CreateDevServerResponse {
|
||||
uint64 dev_server_id = 1;
|
||||
uint64 channel_id = 2;
|
||||
reserved 2;
|
||||
string access_token = 3;
|
||||
string name = 4;
|
||||
}
|
||||
@ -481,6 +492,10 @@ message CreateDevServerResponse {
|
||||
message ShutdownDevServer {
|
||||
}
|
||||
|
||||
message DeleteDevServer {
|
||||
uint64 dev_server_id = 1;
|
||||
}
|
||||
|
||||
message ReconnectDevServer {
|
||||
repeated UpdateProject reshared_projects = 1;
|
||||
}
|
||||
@ -493,6 +508,11 @@ message DevServerInstructions {
|
||||
repeated RemoteProject projects = 1;
|
||||
}
|
||||
|
||||
message RemoteProjectsUpdate {
|
||||
repeated DevServer dev_servers = 1;
|
||||
repeated RemoteProject remote_projects = 2;
|
||||
}
|
||||
|
||||
message ShareRemoteProject {
|
||||
uint64 remote_project_id = 1;
|
||||
repeated WorktreeMetadata worktrees = 2;
|
||||
@ -509,6 +529,7 @@ message JoinProjectResponse {
|
||||
repeated Collaborator collaborators = 3;
|
||||
repeated LanguageServer language_servers = 4;
|
||||
ChannelRole role = 6;
|
||||
optional uint64 remote_project_id = 7;
|
||||
}
|
||||
|
||||
message LeaveProject {
|
||||
@ -1131,11 +1152,10 @@ message UpdateChannels {
|
||||
repeated HostedProject hosted_projects = 10;
|
||||
repeated uint64 deleted_hosted_projects = 11;
|
||||
|
||||
repeated DevServer dev_servers = 12;
|
||||
repeated uint64 deleted_dev_servers = 13;
|
||||
|
||||
repeated RemoteProject remote_projects = 14;
|
||||
repeated uint64 deleted_remote_projects = 15;
|
||||
reserved 12;
|
||||
reserved 13;
|
||||
reserved 14;
|
||||
reserved 15;
|
||||
}
|
||||
|
||||
message UpdateUserChannels {
|
||||
@ -1174,14 +1194,14 @@ message HostedProject {
|
||||
message RemoteProject {
|
||||
uint64 id = 1;
|
||||
optional uint64 project_id = 2;
|
||||
uint64 channel_id = 3;
|
||||
string name = 4;
|
||||
reserved 3;
|
||||
reserved 4;
|
||||
uint64 dev_server_id = 5;
|
||||
string path = 6;
|
||||
}
|
||||
|
||||
message DevServer {
|
||||
uint64 channel_id = 1;
|
||||
reserved 1;
|
||||
uint64 dev_server_id = 2;
|
||||
string name = 3;
|
||||
DevServerStatus status = 4;
|
||||
|
@ -303,7 +303,7 @@ messages!(
|
||||
(SetRoomParticipantRole, Foreground),
|
||||
(BlameBuffer, Foreground),
|
||||
(BlameBufferResponse, Foreground),
|
||||
(CreateRemoteProject, Foreground),
|
||||
(CreateRemoteProject, Background),
|
||||
(CreateRemoteProjectResponse, Foreground),
|
||||
(CreateDevServer, Foreground),
|
||||
(CreateDevServerResponse, Foreground),
|
||||
@ -317,6 +317,9 @@ messages!(
|
||||
(RejoinRemoteProjectsResponse, Foreground),
|
||||
(MultiLspQuery, Background),
|
||||
(MultiLspQueryResponse, Background),
|
||||
(RemoteProjectsUpdate, Foreground),
|
||||
(ValidateRemoteProjectRequest, Background),
|
||||
(DeleteDevServer, Foreground)
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
@ -417,7 +420,9 @@ request_messages!(
|
||||
(JoinRemoteProject, JoinProjectResponse),
|
||||
(RejoinRemoteProjects, RejoinRemoteProjectsResponse),
|
||||
(ReconnectDevServer, ReconnectDevServerResponse),
|
||||
(ValidateRemoteProjectRequest, Ack),
|
||||
(MultiLspQuery, MultiLspQueryResponse),
|
||||
(DeleteDevServer, Ack),
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
@ -105,7 +105,8 @@ impl Connection {
|
||||
let mut raw_statement = ptr::null_mut::<sqlite3_stmt>();
|
||||
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
|
||||
// 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
|
||||
@ -115,9 +116,7 @@ impl Connection {
|
||||
let temp_connection = Connection::open_memory(None);
|
||||
//This should always succeed, if it doesn't then you really should know about it
|
||||
temp_connection
|
||||
.exec(&format!(
|
||||
"CREATE TABLE {table_to_alter}(__place_holder_column_for_syntax_checking)"
|
||||
))
|
||||
.exec(&format!("CREATE TABLE {table_to_alter}({column})"))
|
||||
.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();
|
||||
if remaining_sql_str.starts_with("alter") {
|
||||
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())
|
||||
.collect::<String>();
|
||||
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,
|
||||
callback: impl FnOnce(&mut Statement) -> Result<R>,
|
||||
) -> Result<R> {
|
||||
println!("{:?}", std::any::type_name::<R>());
|
||||
if this.step()? != StepResult::Row {
|
||||
return Err(anyhow!("single called with query that returns no rows."));
|
||||
}
|
||||
|
@ -330,6 +330,7 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
text: hit.string.clone(),
|
||||
highlight_positions: hit.positions.clone(),
|
||||
char_count: hit.string.chars().count(),
|
||||
color: Color::Default,
|
||||
};
|
||||
let icon = match source_kind {
|
||||
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 crate::prelude::*;
|
||||
use crate::{prelude::*, Indicator};
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum IconSize {
|
||||
@ -283,3 +283,63 @@ impl RenderOnce for Icon {
|
||||
.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 crate::{h_flex, IconButton, IconButtonShape, IconName, Label, LabelCommon, LabelSize};
|
||||
use crate::{
|
||||
h_flex, Clickable, IconButton, IconButtonShape, IconName, Label, LabelCommon, LabelSize,
|
||||
};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModalHeader {
|
||||
id: ElementId,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
show_dismiss_button: bool,
|
||||
show_back_button: bool,
|
||||
}
|
||||
|
||||
impl ModalHeader {
|
||||
@ -14,8 +18,20 @@ impl ModalHeader {
|
||||
Self {
|
||||
id: id.into(),
|
||||
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 {
|
||||
@ -31,9 +47,28 @@ impl RenderOnce for ModalHeader {
|
||||
.w_full()
|
||||
.px_2()
|
||||
.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))
|
||||
.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>,
|
||||
/// The layout of the label relative to the text field.
|
||||
with_label: FieldLabelLayout,
|
||||
/// Whether the text field is disabled.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl FocusableView for TextField {
|
||||
@ -72,6 +74,7 @@ impl TextField {
|
||||
editor,
|
||||
start_icon: None,
|
||||
with_label: FieldLabelLayout::Hidden,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,6 +87,16 @@ impl TextField {
|
||||
self.with_label = layout;
|
||||
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 {
|
||||
@ -91,17 +104,17 @@ impl Render for TextField {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let theme_color = cx.theme().colors();
|
||||
|
||||
let style = TextFieldStyle {
|
||||
let mut style = TextFieldStyle {
|
||||
text_color: theme_color.text,
|
||||
background_color: theme_color.ghost_element_background,
|
||||
border_color: theme_color.border,
|
||||
};
|
||||
|
||||
// if self.disabled {
|
||||
// style.text_color = theme_color.text_disabled;
|
||||
// style.background_color = theme_color.ghost_element_disabled;
|
||||
// style.border_color = theme_color.border_disabled;
|
||||
// }
|
||||
if self.disabled {
|
||||
style.text_color = theme_color.text_disabled;
|
||||
style.background_color = theme_color.ghost_element_disabled;
|
||||
style.border_color = theme_color.border_disabled;
|
||||
}
|
||||
|
||||
// if self.error_message.is_some() {
|
||||
// style.text_color = cx.theme().status().error;
|
||||
@ -131,7 +144,15 @@ impl Render for TextField {
|
||||
.group("text-field")
|
||||
.w_full()
|
||||
.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(
|
||||
v_flex().w_full().child(
|
||||
|
@ -45,6 +45,7 @@ node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
remote_projects.workspace = true
|
||||
task.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
@ -513,8 +513,9 @@ impl<T: Item> ItemHandle for View<T> {
|
||||
}));
|
||||
}
|
||||
|
||||
let mut event_subscription =
|
||||
Some(cx.subscribe(self, move |workspace, item, event, cx| {
|
||||
let mut event_subscription = Some(cx.subscribe(
|
||||
self,
|
||||
move |workspace, item: View<T>, event, cx| {
|
||||
let pane = if let Some(pane) = workspace
|
||||
.panes_by_item
|
||||
.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| {
|
||||
if WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange {
|
||||
|
@ -3,6 +3,7 @@ pub mod model;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use client::RemoteProjectId;
|
||||
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
|
||||
use gpui::{point, size, Axis, Bounds};
|
||||
|
||||
@ -17,11 +18,11 @@ use uuid::Uuid;
|
||||
use crate::WorkspaceId;
|
||||
|
||||
use model::{
|
||||
GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
|
||||
WorkspaceLocation,
|
||||
GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
|
||||
SerializedWorkspace,
|
||||
};
|
||||
|
||||
use self::model::DockStructure;
|
||||
use self::model::{DockStructure, SerializedRemoteProject, SerializedWorkspaceLocation};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
|
||||
@ -125,7 +126,7 @@ define_connection! {
|
||||
//
|
||||
// workspaces(
|
||||
// workspace_id: usize, // Primary key for workspaces
|
||||
// workspace_location: Bincode<Vec<PathBuf>>,
|
||||
// local_paths: Bincode<Vec<PathBuf>>,
|
||||
// dock_visible: bool, // Deprecated
|
||||
// dock_anchor: DockAnchor, // Deprecated
|
||||
// dock_pane: Option<usize>, // Deprecated
|
||||
@ -289,6 +290,15 @@ define_connection! {
|
||||
sql!(
|
||||
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,
|
||||
worktree_roots: &[P],
|
||||
) -> 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
|
||||
// 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,
|
||||
WorkspaceLocation,
|
||||
Option<LocalPaths>,
|
||||
Option<u64>,
|
||||
Option<SerializedWindowsBounds>,
|
||||
Option<Uuid>,
|
||||
Option<bool>,
|
||||
@ -316,7 +336,8 @@ impl WorkspaceDb {
|
||||
.select_row_bound(sql! {
|
||||
SELECT
|
||||
workspace_id,
|
||||
workspace_location,
|
||||
local_paths,
|
||||
remote_project_id,
|
||||
window_state,
|
||||
window_x,
|
||||
window_y,
|
||||
@ -335,16 +356,34 @@ impl WorkspaceDb {
|
||||
bottom_dock_active_panel,
|
||||
bottom_dock_zoom
|
||||
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")
|
||||
.warn_on_err()
|
||||
.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 {
|
||||
id: workspace_id,
|
||||
location: workspace_location.clone(),
|
||||
location,
|
||||
center_group: self
|
||||
.get_center_pane_group(workspace_id)
|
||||
.context("Getting center group")
|
||||
@ -368,16 +407,18 @@ impl WorkspaceDb {
|
||||
DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
|
||||
.context("Clearing old panes")?;
|
||||
|
||||
match workspace.location {
|
||||
SerializedWorkspaceLocation::Local(local_paths) => {
|
||||
conn.exec_bound(sql!(
|
||||
DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
|
||||
))?((&workspace.location, workspace.id))
|
||||
DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ?
|
||||
))?((&local_paths, workspace.id))
|
||||
.context("clearing out old locations")?;
|
||||
|
||||
// Upsert
|
||||
conn.exec_bound(sql!(
|
||||
INSERT INTO workspaces(
|
||||
workspace_id,
|
||||
workspace_location,
|
||||
local_paths,
|
||||
left_dock_visible,
|
||||
left_dock_active_panel,
|
||||
left_dock_zoom,
|
||||
@ -392,7 +433,7 @@ impl WorkspaceDb {
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT DO
|
||||
UPDATE SET
|
||||
workspace_location = ?2,
|
||||
local_paths = ?2,
|
||||
left_dock_visible = ?3,
|
||||
left_dock_active_panel = ?4,
|
||||
left_dock_zoom = ?5,
|
||||
@ -403,8 +444,65 @@ impl WorkspaceDb {
|
||||
bottom_dock_active_panel = ?10,
|
||||
bottom_dock_zoom = ?11,
|
||||
timestamp = CURRENT_TIMESTAMP
|
||||
))?((workspace.id, &workspace.location, workspace.docks))
|
||||
))?((workspace.id, &local_paths, workspace.docks))
|
||||
.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
|
||||
Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
|
||||
@ -424,22 +522,41 @@ impl WorkspaceDb {
|
||||
}
|
||||
|
||||
query! {
|
||||
fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
|
||||
SELECT workspace_id, workspace_location
|
||||
fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, Option<u64>)>> {
|
||||
SELECT workspace_id, local_paths, remote_project_id
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub fn last_window() -> Result<(Option<Uuid>, Option<SerializedWindowsBounds>, Option<bool>)> {
|
||||
SELECT display, window_state, window_x, window_y, window_width, window_height, fullscreen
|
||||
fn remote_projects() -> Result<Vec<SerializedRemoteProject>> {
|
||||
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
|
||||
WHERE workspace_location IS NOT NULL
|
||||
WHERE local_paths
|
||||
IS NOT NULL
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1
|
||||
}
|
||||
))?;
|
||||
let result = prepared_query()?;
|
||||
Ok(result
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or_else(|| (None, None, None)))
|
||||
}
|
||||
|
||||
query! {
|
||||
@ -451,14 +568,29 @@ impl WorkspaceDb {
|
||||
|
||||
// Returns the recent locations which are still valid on disk and deletes ones which no longer
|
||||
// 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 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())
|
||||
&& location.paths().iter().any(|path| path.is_dir())
|
||||
{
|
||||
result.push((id, location));
|
||||
result.push((id, location.into()));
|
||||
} else {
|
||||
delete_tasks.push(self.delete_workspace_by_id(id));
|
||||
}
|
||||
@ -468,13 +600,16 @@ impl WorkspaceDb {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn last_workspace(&self) -> Result<Option<WorkspaceLocation>> {
|
||||
pub async fn last_workspace(&self) -> Result<Option<LocalPaths>> {
|
||||
Ok(self
|
||||
.recent_workspaces_on_disk()
|
||||
.await?
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|(_, location)| location))
|
||||
.filter_map(|(_, location)| match location {
|
||||
SerializedWorkspaceLocation::Local(local_paths) => Some(local_paths),
|
||||
SerializedWorkspaceLocation::Remote(_) => None,
|
||||
})
|
||||
.next())
|
||||
}
|
||||
|
||||
fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
|
||||
@ -774,7 +909,7 @@ mod tests {
|
||||
|
||||
let mut workspace_1 = SerializedWorkspace {
|
||||
id: WorkspaceId(1),
|
||||
location: (["/tmp", "/tmp2"]).into(),
|
||||
location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
|
||||
center_group: Default::default(),
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
@ -785,7 +920,7 @@ mod tests {
|
||||
|
||||
let workspace_2 = SerializedWorkspace {
|
||||
id: WorkspaceId(2),
|
||||
location: (["/tmp"]).into(),
|
||||
location: LocalPaths::new(["/tmp"]).into(),
|
||||
center_group: Default::default(),
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
@ -812,7 +947,7 @@ mod tests {
|
||||
})
|
||||
.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).await;
|
||||
db.save_workspace(workspace_2).await;
|
||||
@ -885,7 +1020,7 @@ mod tests {
|
||||
|
||||
let workspace = SerializedWorkspace {
|
||||
id: WorkspaceId(5),
|
||||
location: (["/tmp", "/tmp2"]).into(),
|
||||
location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
|
||||
center_group,
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
@ -915,7 +1050,7 @@ mod tests {
|
||||
|
||||
let workspace_1 = SerializedWorkspace {
|
||||
id: WorkspaceId(1),
|
||||
location: (["/tmp", "/tmp2"]).into(),
|
||||
location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
|
||||
center_group: Default::default(),
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
@ -926,7 +1061,7 @@ mod tests {
|
||||
|
||||
let mut workspace_2 = SerializedWorkspace {
|
||||
id: WorkspaceId(2),
|
||||
location: (["/tmp"]).into(),
|
||||
location: LocalPaths::new(["/tmp"]).into(),
|
||||
center_group: Default::default(),
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
@ -953,7 +1088,7 @@ mod tests {
|
||||
assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
|
||||
|
||||
// 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;
|
||||
assert_eq!(
|
||||
@ -964,7 +1099,7 @@ mod tests {
|
||||
// Test other mechanism for mutating
|
||||
let mut workspace_3 = SerializedWorkspace {
|
||||
id: WorkspaceId(3),
|
||||
location: (&["/tmp", "/tmp2"]).into(),
|
||||
location: LocalPaths::new(&["/tmp", "/tmp2"]).into(),
|
||||
center_group: Default::default(),
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
@ -980,7 +1115,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// 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;
|
||||
assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
|
||||
assert_eq!(
|
||||
@ -999,7 +1134,7 @@ mod tests {
|
||||
) -> SerializedWorkspace {
|
||||
SerializedWorkspace {
|
||||
id: WorkspaceId(4),
|
||||
location: workspace_id.into(),
|
||||
location: LocalPaths::new(workspace_id).into(),
|
||||
center_group: center_group.clone(),
|
||||
bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
|
@ -2,12 +2,14 @@ use super::SerializedAxis;
|
||||
use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
|
||||
use anyhow::{Context, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use client::RemoteProjectId;
|
||||
use db::sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
statement::Statement,
|
||||
};
|
||||
use gpui::{AsyncWindowContext, Bounds, DevicePixels, Model, Task, View, WeakView};
|
||||
use project::Project;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
@ -15,59 +17,98 @@ use std::{
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkspaceLocation(Arc<Vec<PathBuf>>);
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
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>> {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn new<P: AsRef<Path>>(paths: Vec<P>) -> Self {
|
||||
Self(Arc::new(
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|p| p.as_ref().to_path_buf())
|
||||
.collect(),
|
||||
))
|
||||
impl From<LocalPaths> for SerializedWorkspaceLocation {
|
||||
fn from(local_paths: LocalPaths) -> Self {
|
||||
Self::Local(local_paths)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {
|
||||
fn from(iterator: T) -> Self {
|
||||
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 {
|
||||
impl StaticColumnCount for LocalPaths {}
|
||||
impl Bind for &LocalPaths {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
bincode::serialize(&self.0)
|
||||
.expect("Bincode serialization of paths should not fail")
|
||||
.bind(statement, start_index)
|
||||
statement.bind(&bincode::serialize(&self.0)?, start_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for WorkspaceLocation {
|
||||
impl Column for LocalPaths {
|
||||
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((
|
||||
WorkspaceLocation(bincode::deserialize(blob).context("Bincode failed")?),
|
||||
start_index + 1,
|
||||
Self {
|
||||
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)]
|
||||
pub(crate) struct SerializedWorkspace {
|
||||
pub(crate) id: WorkspaceId,
|
||||
pub(crate) location: WorkspaceLocation,
|
||||
pub(crate) location: SerializedWorkspaceLocation,
|
||||
pub(crate) center_group: SerializedPaneGroup,
|
||||
pub(crate) bounds: Option<Bounds<DevicePixels>>,
|
||||
pub(crate) fullscreen: bool,
|
||||
|
@ -46,7 +46,7 @@ pub use pane::*;
|
||||
pub use pane_group::*;
|
||||
use persistence::{model::SerializedWorkspace, SerializedWindowsBounds, DB};
|
||||
pub use persistence::{
|
||||
model::{ItemId, WorkspaceLocation},
|
||||
model::{ItemId, LocalPaths, SerializedRemoteProject, SerializedWorkspaceLocation},
|
||||
WorkspaceDb, DB as WORKSPACE_DB,
|
||||
};
|
||||
use postage::stream::Stream;
|
||||
@ -82,7 +82,7 @@ use ui::{
|
||||
InteractiveElement as _, IntoElement, Label, ParentElement as _, Pixels, SharedString,
|
||||
Styled as _, ViewContext, VisualContext as _, WindowContext,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use util::{maybe, ResultExt};
|
||||
use uuid::Uuid;
|
||||
pub use workspace_settings::{
|
||||
AutosaveSetting, RestoreOnStartupBehaviour, TabBarSettings, WorkspaceSettings,
|
||||
@ -3392,17 +3392,16 @@ impl Workspace {
|
||||
self.database_id
|
||||
}
|
||||
|
||||
fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
|
||||
fn local_paths(&self, cx: &AppContext) -> Option<LocalPaths> {
|
||||
let project = self.project().read(cx);
|
||||
|
||||
if project.is_local() {
|
||||
Some(
|
||||
Some(LocalPaths::new(
|
||||
project
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
)
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -3540,11 +3539,31 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(location) = self.location(cx) {
|
||||
// Load bearing special case:
|
||||
// - with_local_workspace() relies on this to not have other stuff open
|
||||
// when you open your log
|
||||
if !location.paths().is_empty() {
|
||||
let location = if let Some(local_paths) = self.local_paths(cx) {
|
||||
if !local_paths.paths().is_empty() {
|
||||
Some(SerializedWorkspaceLocation::Local(local_paths))
|
||||
} else {
|
||||
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 docks = build_serialized_docks(self, cx);
|
||||
let serialized_workspace = SerializedWorkspace {
|
||||
@ -3559,7 +3578,6 @@ impl Workspace {
|
||||
};
|
||||
return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
|
||||
}
|
||||
}
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
@ -4303,7 +4321,7 @@ pub fn activate_workspace_for_project(
|
||||
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()
|
||||
}
|
||||
|
||||
@ -4410,7 +4428,6 @@ async fn join_channel_internal(
|
||||
if let Some((project, host)) = room.most_active_project(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 room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
|
||||
if let Some(workspace) = requesting_window {
|
||||
@ -4419,7 +4436,7 @@ async fn join_channel_internal(
|
||||
return None;
|
||||
}
|
||||
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| {
|
||||
tree.read(cx)
|
||||
.root_entry()
|
||||
|
@ -71,6 +71,7 @@ project_panel.workspace = true
|
||||
project_symbols.workspace = true
|
||||
quick_action_bar.workspace = true
|
||||
recent_projects.workspace = true
|
||||
remote_projects.workspace = true
|
||||
release_channel.workspace = true
|
||||
rope.workspace = true
|
||||
search.workspace = true
|
||||
|
@ -286,6 +286,7 @@ fn init_ui(args: Args) {
|
||||
ThemeRegistry::global(cx),
|
||||
cx,
|
||||
);
|
||||
remote_projects::init(client.clone(), cx);
|
||||
|
||||
load_user_themes_in_background(fs.clone(), cx);
|
||||
watch_themes(fs.clone(), cx);
|
||||
|
@ -42,6 +42,7 @@ let instanceCount = 1;
|
||||
let isReleaseMode = false;
|
||||
let isTop = false;
|
||||
let othersOnStable = false;
|
||||
let isStateful = false;
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
while (args.length > 0) {
|
||||
@ -52,6 +53,8 @@ while (args.length > 0) {
|
||||
instanceCount = parseInt(digitMatch[1]);
|
||||
} else if (arg === "--release") {
|
||||
isReleaseMode = true;
|
||||
} else if (arg == "--stateful") {
|
||||
isStateful = true;
|
||||
} else if (arg === "--top") {
|
||||
isTop = true;
|
||||
} else if (arg === "--help") {
|
||||
@ -147,7 +150,7 @@ setTimeout(() => {
|
||||
env: {
|
||||
ZED_IMPERSONATE: users[i],
|
||||
ZED_WINDOW_POSITION: position,
|
||||
ZED_STATELESS: "1",
|
||||
ZED_STATELESS: isStateful && i == 0 ? "1" : "",
|
||||
ZED_ALWAYS_ACTIVE: "1",
|
||||
ZED_SERVER_URL: "http://localhost:3000",
|
||||
ZED_RPC_URL: "http://localhost:8080/rpc",
|
||||
|
Loading…
Reference in New Issue
Block a user