remoting: Allow Add/Remove remote folder (#14532)
Release Notes: - remoting (alpha only): Allow add/remove folders to projects --------- Co-authored-by: Max <max@zed.dev>
This commit is contained in:
parent
be1387fee6
commit
62ab6e1a11
32 changed files with 612 additions and 239 deletions
|
@ -414,5 +414,5 @@ CREATE TABLE dev_servers (
|
||||||
CREATE TABLE dev_server_projects (
|
CREATE TABLE dev_server_projects (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
|
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
|
||||||
path TEXT NOT NULL
|
paths TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
ALTER TABLE dev_server_projects ADD COLUMN paths JSONB NULL;
|
||||||
|
UPDATE dev_server_projects SET paths = to_json(ARRAY[path]);
|
||||||
|
ALTER TABLE dev_server_projects ALTER COLUMN paths SET NOT NULL;
|
||||||
|
ALTER TABLE dev_server_projects ALTER COLUMN path DROP NOT NULL;
|
|
@ -5,7 +5,7 @@ use rpc::{
|
||||||
};
|
};
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
|
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
|
||||||
ModelTrait, QueryFilter,
|
IntoActiveModel, ModelTrait, QueryFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::db::ProjectId;
|
use crate::db::ProjectId;
|
||||||
|
@ -56,12 +56,7 @@ impl Database {
|
||||||
.await?;
|
.await?;
|
||||||
Ok(servers
|
Ok(servers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(dev_server_project, project)| proto::DevServerProject {
|
.map(|(dev_server_project, project)| dev_server_project.to_proto(project))
|
||||||
id: dev_server_project.id.to_proto(),
|
|
||||||
project_id: project.map(|p| p.id.to_proto()),
|
|
||||||
dev_server_id: dev_server_project.dev_server_id.to_proto(),
|
|
||||||
path: dev_server_project.path,
|
|
||||||
})
|
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +129,7 @@ impl Database {
|
||||||
let project = dev_server_project::Entity::insert(dev_server_project::ActiveModel {
|
let project = dev_server_project::Entity::insert(dev_server_project::ActiveModel {
|
||||||
id: ActiveValue::NotSet,
|
id: ActiveValue::NotSet,
|
||||||
dev_server_id: ActiveValue::Set(dev_server_id),
|
dev_server_id: ActiveValue::Set(dev_server_id),
|
||||||
path: ActiveValue::Set(path.to_string()),
|
paths: ActiveValue::Set(dev_server_project::JSONPaths(vec![path.to_string()])),
|
||||||
})
|
})
|
||||||
.exec_with_returning(&*tx)
|
.exec_with_returning(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -148,6 +143,38 @@ impl Database {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn update_dev_server_project(
|
||||||
|
&self,
|
||||||
|
id: DevServerProjectId,
|
||||||
|
paths: &Vec<String>,
|
||||||
|
user_id: UserId,
|
||||||
|
) -> crate::Result<(dev_server_project::Model, proto::DevServerProjectsUpdate)> {
|
||||||
|
self.transaction(move |tx| async move {
|
||||||
|
let paths = paths.clone();
|
||||||
|
let Some((project, Some(dev_server))) = dev_server_project::Entity::find_by_id(id)
|
||||||
|
.find_also_related(dev_server::Entity)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
return Err(anyhow!("no such dev server project"))?;
|
||||||
|
};
|
||||||
|
|
||||||
|
if dev_server.user_id != user_id {
|
||||||
|
return Err(anyhow!("not your dev server"))?;
|
||||||
|
}
|
||||||
|
let mut project = project.into_active_model();
|
||||||
|
project.paths = ActiveValue::Set(dev_server_project::JSONPaths(paths));
|
||||||
|
let project = project.update(&*tx).await?;
|
||||||
|
|
||||||
|
let status = self
|
||||||
|
.dev_server_projects_update_internal(user_id, &tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((project, status))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn delete_dev_server_project(
|
pub async fn delete_dev_server_project(
|
||||||
&self,
|
&self,
|
||||||
dev_server_project_id: DevServerProjectId,
|
dev_server_project_id: DevServerProjectId,
|
||||||
|
@ -258,7 +285,6 @@ impl Database {
|
||||||
dev_server_id: DevServerId,
|
dev_server_id: DevServerId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> crate::Result<Vec<ResharedProject>> {
|
) -> crate::Result<Vec<ResharedProject>> {
|
||||||
// todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
|
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
let mut ret = Vec::new();
|
let mut ret = Vec::new();
|
||||||
for reshared_project in reshared_projects {
|
for reshared_project in reshared_projects {
|
||||||
|
@ -322,7 +348,6 @@ impl Database {
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
connection_id: ConnectionId,
|
connection_id: ConnectionId,
|
||||||
) -> crate::Result<Vec<RejoinedProject>> {
|
) -> crate::Result<Vec<RejoinedProject>> {
|
||||||
// todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
|
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
let mut ret = Vec::new();
|
let mut ret = Vec::new();
|
||||||
for rejoined_project in rejoined_projects {
|
for rejoined_project in rejoined_projects {
|
||||||
|
|
|
@ -19,6 +19,28 @@ impl Database {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_dev_server_for_user(
|
||||||
|
&self,
|
||||||
|
dev_server_id: DevServerId,
|
||||||
|
user_id: UserId,
|
||||||
|
) -> crate::Result<dev_server::Model> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
let server = dev_server::Entity::find_by_id(dev_server_id)
|
||||||
|
.one(&*tx)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("no dev server with id {}", dev_server_id))?;
|
||||||
|
if server.user_id != user_id {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"dev server {} is not owned by user {}",
|
||||||
|
dev_server_id,
|
||||||
|
user_id
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
Ok(server)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_dev_servers(&self, user_id: UserId) -> crate::Result<Vec<dev_server::Model>> {
|
pub async fn get_dev_servers(&self, user_id: UserId) -> crate::Result<Vec<dev_server::Model>> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
Ok(dev_server::Entity::find()
|
Ok(dev_server::Entity::find()
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use super::project;
|
use super::project;
|
||||||
use crate::db::{DevServerId, DevServerProjectId};
|
use crate::db::{DevServerId, DevServerProjectId};
|
||||||
use rpc::proto;
|
use rpc::proto;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::{entity::prelude::*, FromJsonQueryResult};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "dev_server_projects")]
|
#[sea_orm(table_name = "dev_server_projects")]
|
||||||
|
@ -9,9 +10,12 @@ pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: DevServerProjectId,
|
pub id: DevServerProjectId,
|
||||||
pub dev_server_id: DevServerId,
|
pub dev_server_id: DevServerId,
|
||||||
pub path: String,
|
pub paths: JSONPaths,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)]
|
||||||
|
pub struct JSONPaths(pub Vec<String>);
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
@ -44,7 +48,12 @@ impl Model {
|
||||||
id: self.id.to_proto(),
|
id: self.id.to_proto(),
|
||||||
project_id: project.map(|p| p.id.to_proto()),
|
project_id: project.map(|p| p.id.to_proto()),
|
||||||
dev_server_id: self.dev_server_id.to_proto(),
|
dev_server_id: self.dev_server_id.to_proto(),
|
||||||
path: self.path.clone(),
|
path: self.paths().get(0).cloned().unwrap_or_default(),
|
||||||
|
paths: self.paths().clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn paths(&self) -> &Vec<String> {
|
||||||
|
&self.paths.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -431,11 +431,13 @@ impl Server {
|
||||||
.add_request_handler(user_handler(join_hosted_project))
|
.add_request_handler(user_handler(join_hosted_project))
|
||||||
.add_request_handler(user_handler(rejoin_dev_server_projects))
|
.add_request_handler(user_handler(rejoin_dev_server_projects))
|
||||||
.add_request_handler(user_handler(create_dev_server_project))
|
.add_request_handler(user_handler(create_dev_server_project))
|
||||||
|
.add_request_handler(user_handler(update_dev_server_project))
|
||||||
.add_request_handler(user_handler(delete_dev_server_project))
|
.add_request_handler(user_handler(delete_dev_server_project))
|
||||||
.add_request_handler(user_handler(create_dev_server))
|
.add_request_handler(user_handler(create_dev_server))
|
||||||
.add_request_handler(user_handler(regenerate_dev_server_token))
|
.add_request_handler(user_handler(regenerate_dev_server_token))
|
||||||
.add_request_handler(user_handler(rename_dev_server))
|
.add_request_handler(user_handler(rename_dev_server))
|
||||||
.add_request_handler(user_handler(delete_dev_server))
|
.add_request_handler(user_handler(delete_dev_server))
|
||||||
|
.add_request_handler(user_handler(list_remote_directory))
|
||||||
.add_request_handler(dev_server_handler(share_dev_server_project))
|
.add_request_handler(dev_server_handler(share_dev_server_project))
|
||||||
.add_request_handler(dev_server_handler(shutdown_dev_server))
|
.add_request_handler(dev_server_handler(shutdown_dev_server))
|
||||||
.add_request_handler(dev_server_handler(reconnect_dev_server))
|
.add_request_handler(dev_server_handler(reconnect_dev_server))
|
||||||
|
@ -2313,6 +2315,69 @@ async fn join_hosted_project(
|
||||||
join_project_internal(response, session, &mut project, &replica_id)
|
join_project_internal(response, session, &mut project, &replica_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_remote_directory(
|
||||||
|
request: proto::ListRemoteDirectory,
|
||||||
|
response: Response<proto::ListRemoteDirectory>,
|
||||||
|
session: UserSession,
|
||||||
|
) -> Result<()> {
|
||||||
|
let dev_server_id = DevServerId(request.dev_server_id as i32);
|
||||||
|
let dev_server_connection_id = session
|
||||||
|
.connection_pool()
|
||||||
|
.await
|
||||||
|
.dev_server_connection_id_supporting(dev_server_id, ZedVersion::with_list_directory())?;
|
||||||
|
|
||||||
|
session
|
||||||
|
.db()
|
||||||
|
.await
|
||||||
|
.get_dev_server_for_user(dev_server_id, session.user_id())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
response.send(
|
||||||
|
session
|
||||||
|
.peer
|
||||||
|
.forward_request(session.connection_id, dev_server_connection_id, request)
|
||||||
|
.await?,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_dev_server_project(
|
||||||
|
request: proto::UpdateDevServerProject,
|
||||||
|
response: Response<proto::UpdateDevServerProject>,
|
||||||
|
session: UserSession,
|
||||||
|
) -> Result<()> {
|
||||||
|
let dev_server_project_id = DevServerProjectId(request.dev_server_project_id as i32);
|
||||||
|
|
||||||
|
let (dev_server_project, update) = session
|
||||||
|
.db()
|
||||||
|
.await
|
||||||
|
.update_dev_server_project(dev_server_project_id, &request.paths, session.user_id())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let projects = session
|
||||||
|
.db()
|
||||||
|
.await
|
||||||
|
.get_projects_for_dev_server(dev_server_project.dev_server_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let dev_server_connection_id = session
|
||||||
|
.connection_pool()
|
||||||
|
.await
|
||||||
|
.dev_server_connection_id_supporting(
|
||||||
|
dev_server_project.dev_server_id,
|
||||||
|
ZedVersion::with_list_directory(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
session.peer.send(
|
||||||
|
dev_server_connection_id,
|
||||||
|
proto::DevServerInstructions { projects },
|
||||||
|
)?;
|
||||||
|
|
||||||
|
send_dev_server_projects_update(session.user_id(), update, &session).await;
|
||||||
|
|
||||||
|
response.send(proto::Ack {})
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_dev_server_project(
|
async fn create_dev_server_project(
|
||||||
request: proto::CreateDevServerProject,
|
request: proto::CreateDevServerProject,
|
||||||
response: Response<proto::CreateDevServerProject>,
|
response: Response<proto::CreateDevServerProject>,
|
||||||
|
|
|
@ -38,6 +38,10 @@ impl ZedVersion {
|
||||||
pub fn with_save_as() -> ZedVersion {
|
pub fn with_save_as() -> ZedVersion {
|
||||||
ZedVersion(SemanticVersion::new(0, 134, 0))
|
ZedVersion(SemanticVersion::new(0, 134, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_list_directory() -> ZedVersion {
|
||||||
|
ZedVersion(SemanticVersion::new(0, 145, 0))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait VersionedMessage {
|
pub trait VersionedMessage {
|
||||||
|
@ -187,6 +191,18 @@ impl ConnectionPool {
|
||||||
self.connected_dev_servers.get(&dev_server_id).copied()
|
self.connected_dev_servers.get(&dev_server_id).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dev_server_connection_id_supporting(
|
||||||
|
&self,
|
||||||
|
dev_server_id: DevServerId,
|
||||||
|
required: ZedVersion,
|
||||||
|
) -> Result<ConnectionId> {
|
||||||
|
match self.connected_dev_servers.get(&dev_server_id) {
|
||||||
|
Some(cid) if self.connections[cid].zed_version >= required => Ok(*cid),
|
||||||
|
Some(_) => Err(anyhow!(proto::ErrorCode::RemoteUpgradeRequired)),
|
||||||
|
None => Err(anyhow!(proto::ErrorCode::DevServerOffline)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn channel_user_ids(
|
pub fn channel_user_ids(
|
||||||
&self,
|
&self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
|
|
|
@ -66,7 +66,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
||||||
.update(cx, |store, cx| {
|
.update(cx, |store, cx| {
|
||||||
let projects = store.dev_server_projects();
|
let projects = store.dev_server_projects();
|
||||||
assert_eq!(projects.len(), 1);
|
assert_eq!(projects.len(), 1);
|
||||||
assert_eq!(projects[0].path, "/remote");
|
assert_eq!(projects[0].paths, vec!["/remote"]);
|
||||||
workspace::join_dev_server_project(
|
workspace::join_dev_server_project(
|
||||||
projects[0].id,
|
projects[0].id,
|
||||||
projects[0].project_id.unwrap(),
|
projects[0].project_id.unwrap(),
|
||||||
|
@ -206,7 +206,7 @@ async fn create_dev_server_project(
|
||||||
.update(cx, |store, cx| {
|
.update(cx, |store, cx| {
|
||||||
let projects = store.dev_server_projects();
|
let projects = store.dev_server_projects();
|
||||||
assert_eq!(projects.len(), 1);
|
assert_eq!(projects.len(), 1);
|
||||||
assert_eq!(projects[0].path, "/remote");
|
assert_eq!(projects[0].paths, vec!["/remote"]);
|
||||||
workspace::join_dev_server_project(
|
workspace::join_dev_server_project(
|
||||||
projects[0].id,
|
projects[0].id,
|
||||||
projects[0].project_id.unwrap(),
|
projects[0].project_id.unwrap(),
|
||||||
|
|
|
@ -1528,7 +1528,7 @@ async fn test_project_reconnect(
|
||||||
});
|
});
|
||||||
let (worktree_a2, _) = project_a1
|
let (worktree_a2, _) = project_a1
|
||||||
.update(cx_a, |p, cx| {
|
.update(cx_a, |p, cx| {
|
||||||
p.find_or_create_local_worktree("/root-1/dir2", true, cx)
|
p.find_or_create_worktree("/root-1/dir2", true, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -1601,7 +1601,7 @@ async fn test_project_reconnect(
|
||||||
});
|
});
|
||||||
let (worktree_a3, _) = project_a1
|
let (worktree_a3, _) = project_a1
|
||||||
.update(cx_a, |p, cx| {
|
.update(cx_a, |p, cx| {
|
||||||
p.find_or_create_local_worktree("/root-1/dir3", true, cx)
|
p.find_or_create_worktree("/root-1/dir3", true, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -1725,7 +1725,7 @@ async fn test_project_reconnect(
|
||||||
// While client B is disconnected, add and remove worktrees from client A's project.
|
// While client B is disconnected, add and remove worktrees from client A's project.
|
||||||
let (worktree_a4, _) = project_a1
|
let (worktree_a4, _) = project_a1
|
||||||
.update(cx_a, |p, cx| {
|
.update(cx_a, |p, cx| {
|
||||||
p.find_or_create_local_worktree("/root-1/dir4", true, cx)
|
p.find_or_create_worktree("/root-1/dir4", true, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -4887,7 +4887,7 @@ async fn test_project_search(
|
||||||
let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
|
let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
|
||||||
let (worktree_2, _) = project_a
|
let (worktree_2, _) = project_a
|
||||||
.update(cx_a, |p, cx| {
|
.update(cx_a, |p, cx| {
|
||||||
p.find_or_create_local_worktree("/root/dir-2", true, cx)
|
p.find_or_create_worktree("/root/dir-2", true, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -581,7 +581,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||||
}
|
}
|
||||||
project
|
project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree(&new_root_path, true, cx)
|
project.find_or_create_worktree(&new_root_path, true, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -805,9 +805,7 @@ impl TestClient {
|
||||||
) -> (Model<Project>, WorktreeId) {
|
) -> (Model<Project>, WorktreeId) {
|
||||||
let project = self.build_empty_local_project(cx);
|
let project = self.build_empty_local_project(cx);
|
||||||
let (worktree, _) = project
|
let (worktree, _) = project
|
||||||
.update(cx, |p, cx| {
|
.update(cx, |p, cx| p.find_or_create_worktree(root_path, true, cx))
|
||||||
p.find_or_create_local_worktree(root_path, true, cx)
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
worktree
|
worktree
|
||||||
|
|
|
@ -20,7 +20,7 @@ pub struct Store {
|
||||||
pub struct DevServerProject {
|
pub struct DevServerProject {
|
||||||
pub id: DevServerProjectId,
|
pub id: DevServerProjectId,
|
||||||
pub project_id: Option<ProjectId>,
|
pub project_id: Option<ProjectId>,
|
||||||
pub path: SharedString,
|
pub paths: Vec<SharedString>,
|
||||||
pub dev_server_id: DevServerId,
|
pub dev_server_id: DevServerId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ impl From<proto::DevServerProject> for DevServerProject {
|
||||||
Self {
|
Self {
|
||||||
id: DevServerProjectId(project.id),
|
id: DevServerProjectId(project.id),
|
||||||
project_id: project.project_id.map(|id| ProjectId(id)),
|
project_id: project.project_id.map(|id| ProjectId(id)),
|
||||||
path: project.path.into(),
|
paths: project.paths.into_iter().map(|path| path.into()).collect(),
|
||||||
dev_server_id: DevServerId(project.dev_server_id),
|
dev_server_id: DevServerId(project.dev_server_id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ impl Store {
|
||||||
.filter(|project| project.dev_server_id == id)
|
.filter(|project| project.dev_server_id == id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
projects.sort_by_key(|p| (p.path.clone(), p.id));
|
projects.sort_by_key(|p| (p.paths.clone(), p.id));
|
||||||
projects
|
projects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ impl Store {
|
||||||
pub fn dev_server_projects(&self) -> Vec<DevServerProject> {
|
pub fn dev_server_projects(&self) -> Vec<DevServerProject> {
|
||||||
let mut projects: Vec<DevServerProject> =
|
let mut projects: Vec<DevServerProject> =
|
||||||
self.dev_server_projects.values().cloned().collect();
|
self.dev_server_projects.values().cloned().collect();
|
||||||
projects.sort_by_key(|p| (p.path.clone(), p.id));
|
projects.sort_by_key(|p| (p.paths.clone(), p.id));
|
||||||
projects
|
projects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -935,7 +935,7 @@ impl Item for Editor {
|
||||||
.context("No path stored for this editor")?;
|
.context("No path stored for this editor")?;
|
||||||
|
|
||||||
let (worktree, path) = project
|
let (worktree, path) = project
|
||||||
.find_local_worktree(&path, cx)
|
.find_worktree(&path, cx)
|
||||||
.with_context(|| format!("No worktree for path: {path:?}"))?;
|
.with_context(|| format!("No worktree for path: {path:?}"))?;
|
||||||
let project_path = ProjectPath {
|
let project_path = ProjectPath {
|
||||||
worktree_id: worktree.read(cx).id(),
|
worktree_id: worktree.read(cx).id(),
|
||||||
|
|
|
@ -79,7 +79,7 @@ impl EditorLspTestContext {
|
||||||
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
|
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
|
||||||
project
|
project
|
||||||
.update(&mut cx, |project, cx| {
|
.update(&mut cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree("/root", true, cx)
|
project.find_or_create_worktree("/root", true, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -19,6 +19,7 @@ use gpui::{
|
||||||
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
|
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||||
};
|
};
|
||||||
use num_format::{Locale, ToFormattedString};
|
use num_format::{Locale, ToFormattedString};
|
||||||
|
use project::DirectoryLister;
|
||||||
use release_channel::ReleaseChannel;
|
use release_channel::ReleaseChannel;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
|
@ -54,13 +55,17 @@ pub fn init(cx: &mut AppContext) {
|
||||||
workspace.add_item_to_active_pane(Box::new(extensions_page), None, cx)
|
workspace.add_item_to_active_pane(Box::new(extensions_page), None, cx)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.register_action(move |_, _: &InstallDevExtension, cx| {
|
.register_action(move |workspace, _: &InstallDevExtension, cx| {
|
||||||
let store = ExtensionStore::global(cx);
|
let store = ExtensionStore::global(cx);
|
||||||
let prompt = cx.prompt_for_paths(gpui::PathPromptOptions {
|
let prompt = workspace.prompt_for_open_path(
|
||||||
files: false,
|
gpui::PathPromptOptions {
|
||||||
directories: true,
|
files: false,
|
||||||
multiple: false,
|
directories: true,
|
||||||
});
|
multiple: false,
|
||||||
|
},
|
||||||
|
DirectoryLister::Local(workspace.app_state().fs.clone()),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
let workspace_handle = cx.view().downgrade();
|
let workspace_handle = cx.view().downgrade();
|
||||||
cx.deref_mut()
|
cx.deref_mut()
|
||||||
|
|
|
@ -679,7 +679,7 @@ impl FileFinderDelegate {
|
||||||
let update_result = project
|
let update_result = project
|
||||||
.update(&mut cx, |project, cx| {
|
.update(&mut cx, |project, cx| {
|
||||||
if let Some((worktree, relative_path)) =
|
if let Some((worktree, relative_path)) =
|
||||||
project.find_local_worktree(query_path, cx)
|
project.find_worktree(query_path, cx)
|
||||||
{
|
{
|
||||||
path_matches.push(ProjectPanelOrdMatch(PathMatch {
|
path_matches.push(ProjectPanelOrdMatch(PathMatch {
|
||||||
score: 1.0,
|
score: 1.0,
|
||||||
|
|
|
@ -767,7 +767,7 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
|
||||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
project.update(cx, |project, cx| {
|
project.update(cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree("/external-src", false, cx)
|
project.find_or_create_worktree("/external-src", false, cx)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -1513,7 +1513,7 @@ async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
|
||||||
project
|
project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project
|
project
|
||||||
.find_or_create_local_worktree("/test/project_2", true, cx)
|
.find_or_create_worktree("/test/project_2", true, cx)
|
||||||
.into_future()
|
.into_future()
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use fuzzy::StringMatchCandidate;
|
use fuzzy::StringMatchCandidate;
|
||||||
use gpui::Model;
|
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{compare_paths, Project};
|
use project::{compare_paths, DirectoryLister};
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{
|
sync::{
|
||||||
|
@ -19,7 +18,7 @@ pub(crate) struct OpenPathPrompt;
|
||||||
|
|
||||||
pub struct OpenPathDelegate {
|
pub struct OpenPathDelegate {
|
||||||
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
|
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
|
||||||
project: Model<Project>,
|
lister: DirectoryLister,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
directory_state: Option<DirectoryState>,
|
directory_state: Option<DirectoryState>,
|
||||||
matches: Vec<usize>,
|
matches: Vec<usize>,
|
||||||
|
@ -35,23 +34,23 @@ struct DirectoryState {
|
||||||
|
|
||||||
impl OpenPathPrompt {
|
impl OpenPathPrompt {
|
||||||
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||||
workspace.set_prompt_for_open_path(Box::new(|workspace, cx| {
|
workspace.set_prompt_for_open_path(Box::new(|workspace, lister, cx| {
|
||||||
let (tx, rx) = futures::channel::oneshot::channel();
|
let (tx, rx) = futures::channel::oneshot::channel();
|
||||||
Self::prompt_for_open_path(workspace, tx, cx);
|
Self::prompt_for_open_path(workspace, lister, tx, cx);
|
||||||
rx
|
rx
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_for_open_path(
|
fn prompt_for_open_path(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
|
lister: DirectoryLister,
|
||||||
tx: oneshot::Sender<Option<Vec<PathBuf>>>,
|
tx: oneshot::Sender<Option<Vec<PathBuf>>>,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
let project = workspace.project().clone();
|
|
||||||
workspace.toggle_modal(cx, |cx| {
|
workspace.toggle_modal(cx, |cx| {
|
||||||
let delegate = OpenPathDelegate {
|
let delegate = OpenPathDelegate {
|
||||||
tx: Some(tx),
|
tx: Some(tx),
|
||||||
project: project.clone(),
|
lister: lister.clone(),
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
directory_state: None,
|
directory_state: None,
|
||||||
matches: Vec::new(),
|
matches: Vec::new(),
|
||||||
|
@ -60,11 +59,7 @@ impl OpenPathPrompt {
|
||||||
};
|
};
|
||||||
|
|
||||||
let picker = Picker::uniform_list(delegate, cx).width(rems(34.));
|
let picker = Picker::uniform_list(delegate, cx).width(rems(34.));
|
||||||
let query = if let Some(worktree) = project.read(cx).visible_worktrees(cx).next() {
|
let query = lister.default_query(cx);
|
||||||
worktree.read(cx).abs_path().to_string_lossy().to_string()
|
|
||||||
} else {
|
|
||||||
"~/".to_string()
|
|
||||||
};
|
|
||||||
picker.set_query(query, cx);
|
picker.set_query(query, cx);
|
||||||
picker
|
picker
|
||||||
});
|
});
|
||||||
|
@ -92,7 +87,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
query: String,
|
query: String,
|
||||||
cx: &mut ViewContext<Picker<Self>>,
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> gpui::Task<()> {
|
) -> gpui::Task<()> {
|
||||||
let project = self.project.clone();
|
let lister = self.lister.clone();
|
||||||
let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
|
let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
|
||||||
(query[..index].to_string(), query[index + 1..].to_string())
|
(query[..index].to_string(), query[index + 1..].to_string())
|
||||||
} else {
|
} else {
|
||||||
|
@ -109,9 +104,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
{
|
{
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(project.update(cx, |project, cx| {
|
Some(lister.list_directory(dir.clone(), cx))
|
||||||
project.completions_for_open_path_query(dir.clone(), cx)
|
|
||||||
}))
|
|
||||||
};
|
};
|
||||||
self.cancel_flag.store(true, atomic::Ordering::Relaxed);
|
self.cancel_flag.store(true, atomic::Ordering::Relaxed);
|
||||||
self.cancel_flag = Arc::new(AtomicBool::new(false));
|
self.cancel_flag = Arc::new(AtomicBool::new(false));
|
||||||
|
@ -127,20 +120,12 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
this.update(&mut cx, |this, _| {
|
this.update(&mut cx, |this, _| {
|
||||||
this.delegate.directory_state = Some(match paths {
|
this.delegate.directory_state = Some(match paths {
|
||||||
Ok(mut paths) => {
|
Ok(mut paths) => {
|
||||||
paths.sort_by(|a, b| {
|
paths.sort_by(|a, b| compare_paths((a, true), (b, true)));
|
||||||
compare_paths(
|
|
||||||
(a.strip_prefix(&dir).unwrap_or(Path::new("")), true),
|
|
||||||
(b.strip_prefix(&dir).unwrap_or(Path::new("")), true),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let match_candidates = paths
|
let match_candidates = paths
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(ix, path)| {
|
.map(|(ix, path)| {
|
||||||
Some(StringMatchCandidate::new(
|
StringMatchCandidate::new(ix, path.to_string_lossy().into())
|
||||||
ix,
|
|
||||||
path.file_name()?.to_string_lossy().into(),
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
@ -213,7 +198,16 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
this.delegate
|
this.delegate
|
||||||
.matches
|
.matches
|
||||||
.extend(matches.into_iter().map(|m| m.candidate_id));
|
.extend(matches.into_iter().map(|m| m.candidate_id));
|
||||||
this.delegate.matches.sort();
|
this.delegate.matches.sort_by_key(|m| {
|
||||||
|
(
|
||||||
|
this.delegate.directory_state.as_ref().and_then(|d| {
|
||||||
|
d.match_candidates
|
||||||
|
.get(*m)
|
||||||
|
.map(|c| !c.string.starts_with(&suffix))
|
||||||
|
}),
|
||||||
|
*m,
|
||||||
|
)
|
||||||
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
|
@ -3,7 +3,7 @@ use client::DevServerProjectId;
|
||||||
use client::{user::UserStore, Client, ClientSettings};
|
use client::{user::UserStore, Client, ClientSettings};
|
||||||
use extension::ExtensionStore;
|
use extension::ExtensionStore;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::Future;
|
use futures::{Future, StreamExt};
|
||||||
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, Task, WeakModel};
|
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, Task, WeakModel};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
|
@ -11,6 +11,7 @@ use postage::stream::Stream;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use rpc::{proto, ErrorCode, TypedEnvelope};
|
use rpc::{proto, ErrorCode, TypedEnvelope};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
|
use std::path::Path;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
|
|
||||||
|
@ -96,6 +97,7 @@ impl DevServer {
|
||||||
cx.weak_model(),
|
cx.weak_model(),
|
||||||
Self::handle_validate_dev_server_project_request,
|
Self::handle_validate_dev_server_project_request,
|
||||||
),
|
),
|
||||||
|
client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory),
|
||||||
client.add_message_handler(cx.weak_model(), Self::handle_shutdown),
|
client.add_message_handler(cx.weak_model(), Self::handle_shutdown),
|
||||||
],
|
],
|
||||||
_maintain_connection: maintain_connection,
|
_maintain_connection: maintain_connection,
|
||||||
|
@ -127,34 +129,43 @@ impl DevServer {
|
||||||
envelope: TypedEnvelope<proto::DevServerInstructions>,
|
envelope: TypedEnvelope<proto::DevServerInstructions>,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (added_projects, removed_projects_ids) = this.read_with(&mut cx, |this, _| {
|
let (added_projects, retained_projects, removed_projects_ids) =
|
||||||
let removed_projects = this
|
this.read_with(&mut cx, |this, _| {
|
||||||
.projects
|
let removed_projects = this
|
||||||
.keys()
|
.projects
|
||||||
.filter(|dev_server_project_id| {
|
.keys()
|
||||||
!envelope
|
.filter(|dev_server_project_id| {
|
||||||
.payload
|
!envelope
|
||||||
.projects
|
.payload
|
||||||
.iter()
|
.projects
|
||||||
.any(|p| p.id == dev_server_project_id.0)
|
.iter()
|
||||||
})
|
.any(|p| p.id == dev_server_project_id.0)
|
||||||
.cloned()
|
})
|
||||||
.collect::<Vec<_>>();
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let added_projects = envelope
|
let mut added_projects = vec![];
|
||||||
.payload
|
let mut retained_projects = vec![];
|
||||||
.projects
|
|
||||||
.into_iter()
|
|
||||||
.filter(|project| !this.projects.contains_key(&DevServerProjectId(project.id)))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
(added_projects, removed_projects)
|
for project in envelope.payload.projects.iter() {
|
||||||
})?;
|
if this.projects.contains_key(&DevServerProjectId(project.id)) {
|
||||||
|
retained_projects.push(project.clone());
|
||||||
|
} else {
|
||||||
|
added_projects.push(project.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(added_projects, retained_projects, removed_projects)
|
||||||
|
})?;
|
||||||
|
|
||||||
for dev_server_project in added_projects {
|
for dev_server_project in added_projects {
|
||||||
DevServer::share_project(this.clone(), &dev_server_project, &mut cx).await?;
|
DevServer::share_project(this.clone(), &dev_server_project, &mut cx).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for dev_server_project in retained_projects {
|
||||||
|
DevServer::update_project(this.clone(), &dev_server_project, &mut cx).await?;
|
||||||
|
}
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
for old_project_id in &removed_projects_ids {
|
for old_project_id in &removed_projects_ids {
|
||||||
this.unshare_project(old_project_id, cx)?;
|
this.unshare_project(old_project_id, cx)?;
|
||||||
|
@ -181,6 +192,24 @@ impl DevServer {
|
||||||
Ok(proto::Ack {})
|
Ok(proto::Ack {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_list_remote_directory(
|
||||||
|
this: Model<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::ListRemoteDirectory>,
|
||||||
|
cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::ListRemoteDirectoryResponse> {
|
||||||
|
let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
|
||||||
|
let fs = cx.read_model(&this, |this, _| this.app_state.fs.clone())?;
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
let mut response = fs.read_dir(Path::new(&expanded)).await?;
|
||||||
|
while let Some(path) = response.next().await {
|
||||||
|
if let Some(file_name) = path?.file_name() {
|
||||||
|
entries.push(file_name.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(proto::ListRemoteDirectoryResponse { entries })
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_shutdown(
|
async fn handle_shutdown(
|
||||||
this: Model<Self>,
|
this: Model<Self>,
|
||||||
_envelope: TypedEnvelope<proto::ShutdownDevServer>,
|
_envelope: TypedEnvelope<proto::ShutdownDevServer>,
|
||||||
|
@ -221,17 +250,19 @@ impl DevServer {
|
||||||
(this.client.clone(), project)
|
(this.client.clone(), project)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let path = shellexpand::tilde(&dev_server_project.path).to_string();
|
for path in &dev_server_project.paths {
|
||||||
|
let path = shellexpand::tilde(path).to_string();
|
||||||
|
|
||||||
let (worktree, _) = project
|
let (worktree, _) = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree(&path, true, cx)
|
project.find_or_create_worktree(&path, true, cx)
|
||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
worktree.update(cx, |worktree, cx| {
|
worktree.update(cx, |worktree, cx| {
|
||||||
worktree.as_local_mut().unwrap().share_private_files(cx)
|
worktree.as_local_mut().unwrap().share_private_files(cx)
|
||||||
})?;
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
let worktrees =
|
let worktrees =
|
||||||
project.read_with(cx, |project, cx| project.worktree_metadata_protos(cx))?;
|
project.read_with(cx, |project, cx| project.worktree_metadata_protos(cx))?;
|
||||||
|
@ -252,6 +283,56 @@ impl DevServer {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_project(
|
||||||
|
this: Model<Self>,
|
||||||
|
dev_server_project: &proto::DevServerProject,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let tasks = this.update(cx, |this, cx| {
|
||||||
|
let Some(project) = this
|
||||||
|
.projects
|
||||||
|
.get(&DevServerProjectId(dev_server_project.id))
|
||||||
|
else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut to_delete = vec![];
|
||||||
|
let mut tasks = vec![];
|
||||||
|
|
||||||
|
project.update(cx, |project, cx| {
|
||||||
|
for worktree in project.visible_worktrees(cx) {
|
||||||
|
let mut delete = true;
|
||||||
|
for config in dev_server_project.paths.iter() {
|
||||||
|
if worktree.read(cx).abs_path().to_string_lossy()
|
||||||
|
== shellexpand::tilde(config)
|
||||||
|
{
|
||||||
|
delete = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if delete {
|
||||||
|
to_delete.push(worktree.read(cx).id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for worktree_id in to_delete {
|
||||||
|
project.remove_worktree(worktree_id, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
for config in dev_server_project.paths.iter() {
|
||||||
|
tasks.push(project.find_or_create_worktree(
|
||||||
|
&shellexpand::tilde(config).to_string(),
|
||||||
|
true,
|
||||||
|
cx,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
futures::future::join_all(tasks).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn maintain_connection(
|
async fn maintain_connection(
|
||||||
this: WeakModel<Self>,
|
this: WeakModel<Self>,
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
|
|
|
@ -29,7 +29,7 @@ use futures::{
|
||||||
future::{join_all, try_join_all, Shared},
|
future::{join_all, try_join_all, Shared},
|
||||||
select,
|
select,
|
||||||
stream::FuturesUnordered,
|
stream::FuturesUnordered,
|
||||||
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
AsyncWriteExt, Future, FutureExt, StreamExt,
|
||||||
};
|
};
|
||||||
use fuzzy::CharBag;
|
use fuzzy::CharBag;
|
||||||
use git::{blame::Blame, repository::GitRepository};
|
use git::{blame::Blame, repository::GitRepository};
|
||||||
|
@ -86,6 +86,7 @@ use snippet::Snippet;
|
||||||
use snippet_provider::SnippetProvider;
|
use snippet_provider::SnippetProvider;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
cell::RefCell,
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
convert::TryInto,
|
convert::TryInto,
|
||||||
env,
|
env,
|
||||||
|
@ -202,7 +203,7 @@ pub struct Project {
|
||||||
_subscriptions: Vec<gpui::Subscription>,
|
_subscriptions: Vec<gpui::Subscription>,
|
||||||
shared_buffers: HashMap<proto::PeerId, HashSet<BufferId>>,
|
shared_buffers: HashMap<proto::PeerId, HashSet<BufferId>>,
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
loading_local_worktrees:
|
loading_worktrees:
|
||||||
HashMap<Arc<Path>, Shared<Task<Result<Model<Worktree>, Arc<anyhow::Error>>>>>,
|
HashMap<Arc<Path>, Shared<Task<Result<Model<Worktree>, Arc<anyhow::Error>>>>>,
|
||||||
buffer_snapshots: HashMap<BufferId, HashMap<LanguageServerId, Vec<LspBufferSnapshot>>>, // buffer_id -> server_id -> vec of snapshots
|
buffer_snapshots: HashMap<BufferId, HashMap<LanguageServerId, Vec<LspBufferSnapshot>>>, // buffer_id -> server_id -> vec of snapshots
|
||||||
buffers_being_formatted: HashSet<BufferId>,
|
buffers_being_formatted: HashSet<BufferId>,
|
||||||
|
@ -602,6 +603,52 @@ impl FormatTrigger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum DirectoryLister {
|
||||||
|
Project(Model<Project>),
|
||||||
|
Local(Arc<dyn Fs>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirectoryLister {
|
||||||
|
pub fn is_local(&self, cx: &AppContext) -> bool {
|
||||||
|
match self {
|
||||||
|
DirectoryLister::Local(_) => true,
|
||||||
|
DirectoryLister::Project(project) => project.read(cx).is_local(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_query(&self, cx: &mut AppContext) -> String {
|
||||||
|
if let DirectoryLister::Project(project) = self {
|
||||||
|
if let Some(worktree) = project.read(cx).visible_worktrees(cx).next() {
|
||||||
|
return worktree.read(cx).abs_path().to_string_lossy().to_string();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
"~/".to_string()
|
||||||
|
}
|
||||||
|
pub fn list_directory(&self, query: String, cx: &mut AppContext) -> Task<Result<Vec<PathBuf>>> {
|
||||||
|
match self {
|
||||||
|
DirectoryLister::Project(project) => {
|
||||||
|
project.update(cx, |project, cx| project.list_directory(query, cx))
|
||||||
|
}
|
||||||
|
DirectoryLister::Local(fs) => {
|
||||||
|
let fs = fs.clone();
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
let mut results = vec![];
|
||||||
|
let expanded = shellexpand::tilde(&query);
|
||||||
|
let query = Path::new(expanded.as_ref());
|
||||||
|
let mut response = fs.read_dir(query).await?;
|
||||||
|
while let Some(path) = response.next().await {
|
||||||
|
if let Some(file_name) = path?.file_name() {
|
||||||
|
results.push(PathBuf::from(file_name.to_os_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(results)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
enum SearchMatchCandidate {
|
enum SearchMatchCandidate {
|
||||||
OpenBuffer {
|
OpenBuffer {
|
||||||
|
@ -727,7 +774,7 @@ impl Project {
|
||||||
collaborators: Default::default(),
|
collaborators: Default::default(),
|
||||||
buffer_store,
|
buffer_store,
|
||||||
shared_buffers: Default::default(),
|
shared_buffers: Default::default(),
|
||||||
loading_local_worktrees: Default::default(),
|
loading_worktrees: Default::default(),
|
||||||
buffer_snapshots: Default::default(),
|
buffer_snapshots: Default::default(),
|
||||||
join_project_response_message_id: 0,
|
join_project_response_message_id: 0,
|
||||||
client_state: ProjectClientState::Local,
|
client_state: ProjectClientState::Local,
|
||||||
|
@ -866,7 +913,7 @@ impl Project {
|
||||||
buffer_ordered_messages_tx: tx,
|
buffer_ordered_messages_tx: tx,
|
||||||
buffer_store,
|
buffer_store,
|
||||||
shared_buffers: Default::default(),
|
shared_buffers: Default::default(),
|
||||||
loading_local_worktrees: Default::default(),
|
loading_worktrees: Default::default(),
|
||||||
active_entry: None,
|
active_entry: None,
|
||||||
collaborators: Default::default(),
|
collaborators: Default::default(),
|
||||||
join_project_response_message_id: response.message_id,
|
join_project_response_message_id: response.message_id,
|
||||||
|
@ -1068,7 +1115,7 @@ impl Project {
|
||||||
for path in root_paths {
|
for path in root_paths {
|
||||||
let (tree, _) = project
|
let (tree, _) = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree(path, true, cx)
|
project.find_or_create_worktree(path, true, cx)
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.await
|
.await
|
||||||
|
@ -1106,7 +1153,7 @@ impl Project {
|
||||||
for path in root_paths {
|
for path in root_paths {
|
||||||
let (tree, _) = project
|
let (tree, _) = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree(path, true, cx)
|
project.find_or_create_worktree(path, true, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -1909,7 +1956,7 @@ impl Project {
|
||||||
abs_path: impl AsRef<Path>,
|
abs_path: impl AsRef<Path>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Model<Buffer>>> {
|
) -> Task<Result<Model<Buffer>>> {
|
||||||
if let Some((worktree, relative_path)) = self.find_local_worktree(abs_path.as_ref(), cx) {
|
if let Some((worktree, relative_path)) = self.find_worktree(abs_path.as_ref(), cx) {
|
||||||
self.open_buffer((worktree.read(cx).id(), relative_path), cx)
|
self.open_buffer((worktree.read(cx).id(), relative_path), cx)
|
||||||
} else {
|
} else {
|
||||||
Task::ready(Err(anyhow!("no such path")))
|
Task::ready(Err(anyhow!("no such path")))
|
||||||
|
@ -1976,7 +2023,7 @@ impl Project {
|
||||||
};
|
};
|
||||||
let (worktree, relative_path) = if let Some(result) = this
|
let (worktree, relative_path) = if let Some(result) = this
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| {
|
||||||
this.find_local_worktree(&worktree_root_target, cx)
|
this.find_worktree(&worktree_root_target, cx)
|
||||||
})? {
|
})? {
|
||||||
let relative_path =
|
let relative_path =
|
||||||
known_relative_path.unwrap_or_else(|| Arc::<Path>::from(result.1));
|
known_relative_path.unwrap_or_else(|| Arc::<Path>::from(result.1));
|
||||||
|
@ -1984,7 +2031,7 @@ impl Project {
|
||||||
} else {
|
} else {
|
||||||
let worktree = this
|
let worktree = this
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| {
|
||||||
this.create_local_worktree(&worktree_root_target, false, cx)
|
this.create_worktree(&worktree_root_target, false, cx)
|
||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
|
@ -4572,7 +4619,7 @@ impl Project {
|
||||||
cx: &mut ModelContext<Project>,
|
cx: &mut ModelContext<Project>,
|
||||||
) -> Result<(), anyhow::Error> {
|
) -> Result<(), anyhow::Error> {
|
||||||
let (worktree, relative_path) = self
|
let (worktree, relative_path) = self
|
||||||
.find_local_worktree(&abs_path, cx)
|
.find_worktree(&abs_path, cx)
|
||||||
.ok_or_else(|| anyhow!("no worktree found for diagnostics path {abs_path:?}"))?;
|
.ok_or_else(|| anyhow!("no worktree found for diagnostics path {abs_path:?}"))?;
|
||||||
|
|
||||||
let project_path = ProjectPath {
|
let project_path = ProjectPath {
|
||||||
|
@ -5440,9 +5487,7 @@ impl Project {
|
||||||
|
|
||||||
let path;
|
let path;
|
||||||
let worktree;
|
let worktree;
|
||||||
if let Some((tree, rel_path)) =
|
if let Some((tree, rel_path)) = this.find_worktree(&abs_path, cx) {
|
||||||
this.find_local_worktree(&abs_path, cx)
|
|
||||||
{
|
|
||||||
worktree = tree;
|
worktree = tree;
|
||||||
path = rel_path;
|
path = rel_path;
|
||||||
} else {
|
} else {
|
||||||
|
@ -7516,23 +7561,23 @@ impl Project {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_or_create_local_worktree(
|
pub fn find_or_create_worktree(
|
||||||
&mut self,
|
&mut self,
|
||||||
abs_path: impl AsRef<Path>,
|
abs_path: impl AsRef<Path>,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<(Model<Worktree>, PathBuf)>> {
|
) -> Task<Result<(Model<Worktree>, PathBuf)>> {
|
||||||
let abs_path = abs_path.as_ref();
|
let abs_path = abs_path.as_ref();
|
||||||
if let Some((tree, relative_path)) = self.find_local_worktree(abs_path, cx) {
|
if let Some((tree, relative_path)) = self.find_worktree(abs_path, cx) {
|
||||||
Task::ready(Ok((tree, relative_path)))
|
Task::ready(Ok((tree, relative_path)))
|
||||||
} else {
|
} else {
|
||||||
let worktree = self.create_local_worktree(abs_path, visible, cx);
|
let worktree = self.create_worktree(abs_path, visible, cx);
|
||||||
cx.background_executor()
|
cx.background_executor()
|
||||||
.spawn(async move { Ok((worktree.await?, PathBuf::new())) })
|
.spawn(async move { Ok((worktree.await?, PathBuf::new())) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_local_worktree(
|
pub fn find_worktree(
|
||||||
&self,
|
&self,
|
||||||
abs_path: &Path,
|
abs_path: &Path,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
|
@ -7559,21 +7604,56 @@ impl Project {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn completions_for_open_path_query(
|
pub fn list_directory(
|
||||||
&self,
|
&self,
|
||||||
query: String,
|
query: String,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Vec<PathBuf>>> {
|
) -> Task<Result<Vec<PathBuf>>> {
|
||||||
let fs = self.fs.clone();
|
if self.is_local() {
|
||||||
|
DirectoryLister::Local(self.fs.clone()).list_directory(query, cx)
|
||||||
|
} else if let Some(dev_server) = self.dev_server_project_id().and_then(|id| {
|
||||||
|
dev_server_projects::Store::global(cx)
|
||||||
|
.read(cx)
|
||||||
|
.dev_server_for_project(id)
|
||||||
|
}) {
|
||||||
|
let request = proto::ListRemoteDirectory {
|
||||||
|
dev_server_id: dev_server.id.0,
|
||||||
|
path: query,
|
||||||
|
};
|
||||||
|
let response = self.client.request(request);
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
let response = response.await?;
|
||||||
|
Ok(response.entries.into_iter().map(PathBuf::from).collect())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Task::ready(Err(anyhow!("cannot list directory in remote project")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_worktree(
|
||||||
|
&mut self,
|
||||||
|
abs_path: impl AsRef<Path>,
|
||||||
|
visible: bool,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<Model<Worktree>>> {
|
||||||
|
let path: Arc<Path> = abs_path.as_ref().into();
|
||||||
|
if !self.loading_worktrees.contains_key(&path) {
|
||||||
|
let task = if self.is_local() {
|
||||||
|
self.create_local_worktree(abs_path, visible, cx)
|
||||||
|
} else if self.dev_server_project_id.is_some() {
|
||||||
|
self.create_dev_server_worktree(abs_path, cx)
|
||||||
|
} else {
|
||||||
|
return Task::ready(Err(anyhow!("not a local project")));
|
||||||
|
};
|
||||||
|
self.loading_worktrees.insert(path.clone(), task.shared());
|
||||||
|
}
|
||||||
|
let task = self.loading_worktrees.get(&path).unwrap().clone();
|
||||||
cx.background_executor().spawn(async move {
|
cx.background_executor().spawn(async move {
|
||||||
let mut results = vec![];
|
let result = match task.await {
|
||||||
let expanded = shellexpand::tilde(&query);
|
Ok(worktree) => Ok(worktree),
|
||||||
let query = Path::new(expanded.as_ref());
|
Err(err) => Err(anyhow!("{}", err)),
|
||||||
let mut response = fs.read_dir(query).await?;
|
};
|
||||||
while let Some(path) = response.next().await {
|
result
|
||||||
results.push(path?);
|
|
||||||
}
|
|
||||||
Ok(results)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7582,51 +7662,102 @@ impl Project {
|
||||||
abs_path: impl AsRef<Path>,
|
abs_path: impl AsRef<Path>,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Model<Worktree>>> {
|
) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
let next_entry_id = self.next_entry_id.clone();
|
let next_entry_id = self.next_entry_id.clone();
|
||||||
let path: Arc<Path> = abs_path.as_ref().into();
|
let path: Arc<Path> = abs_path.as_ref().into();
|
||||||
let task = self
|
|
||||||
.loading_local_worktrees
|
|
||||||
.entry(path.clone())
|
|
||||||
.or_insert_with(|| {
|
|
||||||
cx.spawn(move |project, mut cx| {
|
|
||||||
async move {
|
|
||||||
let worktree =
|
|
||||||
Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
project.update(&mut cx, |project, _| {
|
cx.spawn(move |project, mut cx| async move {
|
||||||
project.loading_local_worktrees.remove(&path);
|
let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await;
|
||||||
})?;
|
|
||||||
|
|
||||||
let worktree = worktree?;
|
project.update(&mut cx, |project, _| {
|
||||||
project
|
project.loading_worktrees.remove(&path);
|
||||||
.update(&mut cx, |project, cx| project.add_worktree(&worktree, cx))?;
|
})?;
|
||||||
|
|
||||||
if visible {
|
let worktree = worktree?;
|
||||||
cx.update(|cx| {
|
project.update(&mut cx, |project, cx| project.add_worktree(&worktree, cx))?;
|
||||||
cx.add_recent_document(&path);
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(worktree)
|
if visible {
|
||||||
}
|
cx.update(|cx| {
|
||||||
.map_err(Arc::new)
|
cx.add_recent_document(&path);
|
||||||
})
|
})
|
||||||
.shared()
|
.log_err();
|
||||||
})
|
|
||||||
.clone();
|
|
||||||
cx.background_executor().spawn(async move {
|
|
||||||
match task.await {
|
|
||||||
Ok(worktree) => Ok(worktree),
|
|
||||||
Err(err) => Err(anyhow!("{}", err)),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(worktree)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_dev_server_worktree(
|
||||||
|
&mut self,
|
||||||
|
abs_path: impl AsRef<Path>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
|
||||||
|
let client = self.client.clone();
|
||||||
|
let path: Arc<Path> = abs_path.as_ref().into();
|
||||||
|
let mut paths: Vec<String> = self
|
||||||
|
.visible_worktrees(cx)
|
||||||
|
.map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string())
|
||||||
|
.collect();
|
||||||
|
paths.push(path.to_string_lossy().to_string());
|
||||||
|
let request = client.request(proto::UpdateDevServerProject {
|
||||||
|
dev_server_project_id: self.dev_server_project_id.unwrap().0,
|
||||||
|
paths,
|
||||||
|
});
|
||||||
|
|
||||||
|
let abs_path = abs_path.as_ref().to_path_buf();
|
||||||
|
cx.spawn(move |project, mut cx| async move {
|
||||||
|
let (tx, rx) = futures::channel::oneshot::channel();
|
||||||
|
let tx = RefCell::new(Some(tx));
|
||||||
|
let Some(project) = project.upgrade() else {
|
||||||
|
return Err(anyhow!("project dropped"))?;
|
||||||
|
};
|
||||||
|
let observer = cx.update(|cx| {
|
||||||
|
cx.observe(&project, move |project, cx| {
|
||||||
|
let abs_path = abs_path.clone();
|
||||||
|
project.update(cx, |project, cx| {
|
||||||
|
if let Some((worktree, _)) = project.find_worktree(&abs_path, cx) {
|
||||||
|
if let Some(tx) = tx.borrow_mut().take() {
|
||||||
|
tx.send(worktree).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
request.await?;
|
||||||
|
let worktree = rx.await.map_err(|e| anyhow!(e))?;
|
||||||
|
drop(observer);
|
||||||
|
project.update(&mut cx, |project, _| {
|
||||||
|
project.loading_worktrees.remove(&path);
|
||||||
|
})?;
|
||||||
|
Ok(worktree)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
|
pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
|
||||||
|
if let Some(dev_server_project_id) = self.dev_server_project_id {
|
||||||
|
let paths: Vec<String> = self
|
||||||
|
.visible_worktrees(cx)
|
||||||
|
.filter_map(|worktree| {
|
||||||
|
if worktree.read(cx).id() == id_to_remove {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(worktree.read(cx).abs_path().to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if paths.len() > 0 {
|
||||||
|
let request = self.client.request(proto::UpdateDevServerProject {
|
||||||
|
dev_server_project_id: dev_server_project_id.0,
|
||||||
|
paths,
|
||||||
|
});
|
||||||
|
cx.background_executor()
|
||||||
|
.spawn(request)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.diagnostics.remove(&id_to_remove);
|
self.diagnostics.remove(&id_to_remove);
|
||||||
self.diagnostic_summaries.remove(&id_to_remove);
|
self.diagnostic_summaries.remove(&id_to_remove);
|
||||||
|
|
||||||
|
@ -8278,18 +8409,6 @@ impl Project {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn project_path_for_absolute_path(
|
|
||||||
&self,
|
|
||||||
abs_path: &Path,
|
|
||||||
cx: &AppContext,
|
|
||||||
) -> Option<ProjectPath> {
|
|
||||||
self.find_local_worktree(abs_path, cx)
|
|
||||||
.map(|(worktree, relative_path)| ProjectPath {
|
|
||||||
worktree_id: worktree.read(cx).id(),
|
|
||||||
path: relative_path.into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_workspace_root(
|
pub fn get_workspace_root(
|
||||||
&self,
|
&self,
|
||||||
project_path: &ProjectPath,
|
project_path: &ProjectPath,
|
||||||
|
|
|
@ -987,7 +987,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||||
let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
|
let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
|
||||||
let (worktree, _) = project
|
let (worktree, _) = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree("/root/dir", true, cx)
|
project.find_or_create_worktree("/root/dir", true, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -995,7 +995,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||||
|
|
||||||
let (worktree, _) = project
|
let (worktree, _) = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree("/root/other.rs", false, cx)
|
project.find_or_create_worktree("/root/other.rs", false, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -55,7 +55,9 @@ impl Project {
|
||||||
} else {
|
} else {
|
||||||
projects_store
|
projects_store
|
||||||
.dev_server_project(dev_server_project_id)?
|
.dev_server_project(dev_server_project_id)?
|
||||||
.path
|
.paths
|
||||||
|
.get(0)
|
||||||
|
.unwrap()
|
||||||
.to_string()
|
.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -81,8 +83,8 @@ impl Project {
|
||||||
.and_then(|cwd| cwd.local_path());
|
.and_then(|cwd| cwd.local_path());
|
||||||
|
|
||||||
terminal_cwd
|
terminal_cwd
|
||||||
.and_then(|terminal_cwd| self.find_local_worktree(&terminal_cwd, cx))
|
.and_then(|terminal_cwd| self.find_worktree(&terminal_cwd, cx))
|
||||||
.or_else(|| task_cwd.and_then(|spawn_cwd| self.find_local_worktree(&spawn_cwd, cx)))
|
.or_else(|| task_cwd.and_then(|spawn_cwd| self.find_worktree(&spawn_cwd, cx)))
|
||||||
};
|
};
|
||||||
|
|
||||||
let settings_location = worktree.as_ref().map(|(worktree, path)| SettingsLocation {
|
let settings_location = worktree.as_ref().map(|(worktree, path)| SettingsLocation {
|
||||||
|
|
|
@ -460,9 +460,8 @@ impl ProjectPanel {
|
||||||
let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
|
let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
|
||||||
let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
|
let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
|
||||||
let worktree_id = worktree.id();
|
let worktree_id = worktree.id();
|
||||||
let is_local = project.is_local();
|
|
||||||
let is_read_only = project.is_read_only();
|
let is_read_only = project.is_read_only();
|
||||||
let is_remote = project.is_remote();
|
let is_remote = project.is_remote() && project.dev_server_project_id().is_none();
|
||||||
|
|
||||||
let context_menu = ContextMenu::build(cx, |menu, cx| {
|
let context_menu = ContextMenu::build(cx, |menu, cx| {
|
||||||
menu.context(self.focus_handle.clone()).when_else(
|
menu.context(self.focus_handle.clone()).when_else(
|
||||||
|
@ -526,14 +525,12 @@ impl ProjectPanel {
|
||||||
menu.action("Trash", Box::new(Trash { skip_prompt: false }))
|
menu.action("Trash", Box::new(Trash { skip_prompt: false }))
|
||||||
.action("Delete", Box::new(Delete { skip_prompt: false }))
|
.action("Delete", Box::new(Delete { skip_prompt: false }))
|
||||||
})
|
})
|
||||||
.when(is_local & is_root, |menu| {
|
.when(!is_remote & is_root, |menu| {
|
||||||
menu.separator()
|
menu.separator()
|
||||||
.when(!is_remote, |menu| {
|
.action(
|
||||||
menu.action(
|
"Add Folder to Project…",
|
||||||
"Add Folder to Project…",
|
Box::new(workspace::AddFolderToProject),
|
||||||
Box::new(workspace::AddFolderToProject),
|
)
|
||||||
)
|
|
||||||
})
|
|
||||||
.entry(
|
.entry(
|
||||||
"Remove from Project",
|
"Remove from Project",
|
||||||
None,
|
None,
|
||||||
|
@ -544,7 +541,7 @@ impl ProjectPanel {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(is_local & is_root, |menu| {
|
.when(is_root, |menu| {
|
||||||
menu.separator()
|
menu.separator()
|
||||||
.action("Collapse All", Box::new(CollapseAllEntries))
|
.action("Collapse All", Box::new(CollapseAllEntries))
|
||||||
})
|
})
|
||||||
|
|
|
@ -265,7 +265,10 @@ message Envelope {
|
||||||
SynchronizeContextsResponse synchronize_contexts_response = 216;
|
SynchronizeContextsResponse synchronize_contexts_response = 216;
|
||||||
|
|
||||||
GetSignatureHelp get_signature_help = 217;
|
GetSignatureHelp get_signature_help = 217;
|
||||||
GetSignatureHelpResponse get_signature_help_response = 218; // current max
|
GetSignatureHelpResponse get_signature_help_response = 218;
|
||||||
|
ListRemoteDirectory list_remote_directory = 219;
|
||||||
|
ListRemoteDirectoryResponse list_remote_directory_response = 220;
|
||||||
|
UpdateDevServerProject update_dev_server_project = 221; // current max
|
||||||
}
|
}
|
||||||
|
|
||||||
reserved 158 to 161;
|
reserved 158 to 161;
|
||||||
|
@ -507,6 +510,20 @@ message ValidateDevServerProjectRequest {
|
||||||
string path = 1;
|
string path = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ListRemoteDirectory {
|
||||||
|
uint64 dev_server_id = 1;
|
||||||
|
string path = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListRemoteDirectoryResponse {
|
||||||
|
repeated string entries = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateDevServerProject {
|
||||||
|
uint64 dev_server_project_id = 1;
|
||||||
|
repeated string paths = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message CreateDevServer {
|
message CreateDevServer {
|
||||||
reserved 1;
|
reserved 1;
|
||||||
string name = 2;
|
string name = 2;
|
||||||
|
@ -1335,6 +1352,7 @@ message DevServerProject {
|
||||||
reserved 4;
|
reserved 4;
|
||||||
uint64 dev_server_id = 5;
|
uint64 dev_server_id = 5;
|
||||||
string path = 6;
|
string path = 6;
|
||||||
|
repeated string paths = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DevServer {
|
message DevServer {
|
||||||
|
|
|
@ -377,6 +377,9 @@ messages!(
|
||||||
(MultiLspQueryResponse, Background),
|
(MultiLspQueryResponse, Background),
|
||||||
(DevServerProjectsUpdate, Foreground),
|
(DevServerProjectsUpdate, Foreground),
|
||||||
(ValidateDevServerProjectRequest, Background),
|
(ValidateDevServerProjectRequest, Background),
|
||||||
|
(ListRemoteDirectory, Background),
|
||||||
|
(ListRemoteDirectoryResponse, Background),
|
||||||
|
(UpdateDevServerProject, Background),
|
||||||
(DeleteDevServer, Foreground),
|
(DeleteDevServer, Foreground),
|
||||||
(DeleteDevServerProject, Foreground),
|
(DeleteDevServerProject, Foreground),
|
||||||
(RegenerateDevServerToken, Foreground),
|
(RegenerateDevServerToken, Foreground),
|
||||||
|
@ -434,6 +437,8 @@ request_messages!(
|
||||||
(GetSupermavenApiKey, GetSupermavenApiKeyResponse),
|
(GetSupermavenApiKey, GetSupermavenApiKeyResponse),
|
||||||
(GetTypeDefinition, GetTypeDefinitionResponse),
|
(GetTypeDefinition, GetTypeDefinitionResponse),
|
||||||
(LinkedEditingRange, LinkedEditingRangeResponse),
|
(LinkedEditingRange, LinkedEditingRangeResponse),
|
||||||
|
(ListRemoteDirectory, ListRemoteDirectoryResponse),
|
||||||
|
(UpdateDevServerProject, Ack),
|
||||||
(GetUsers, UsersResponse),
|
(GetUsers, UsersResponse),
|
||||||
(IncomingCall, Ack),
|
(IncomingCall, Ack),
|
||||||
(InlayHints, InlayHintsResponse),
|
(InlayHints, InlayHintsResponse),
|
||||||
|
|
|
@ -173,16 +173,13 @@ impl DevServerProjects {
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.projects_for_server(dev_server_id)
|
.projects_for_server(dev_server_id)
|
||||||
.iter()
|
.iter()
|
||||||
.any(|p| p.path == path)
|
.any(|p| p.paths.iter().any(|p| p == &path))
|
||||||
{
|
{
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
cx.prompt(
|
cx.prompt(
|
||||||
gpui::PromptLevel::Critical,
|
gpui::PromptLevel::Critical,
|
||||||
"Failed to create project",
|
"Failed to create project",
|
||||||
Some(&format!(
|
Some(&format!("{} is already open on this dev server.", path)),
|
||||||
"Project {} already exists for this dev server.",
|
|
||||||
path
|
|
||||||
)),
|
|
||||||
&["Ok"],
|
&["Ok"],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -454,15 +451,10 @@ impl DevServerProjects {
|
||||||
.detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None);
|
.detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_dev_server_project(
|
fn delete_dev_server_project(&mut self, id: DevServerProjectId, cx: &mut ViewContext<Self>) {
|
||||||
&mut self,
|
|
||||||
id: DevServerProjectId,
|
|
||||||
path: &str,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
let answer = cx.prompt(
|
let answer = cx.prompt(
|
||||||
gpui::PromptLevel::Warning,
|
gpui::PromptLevel::Warning,
|
||||||
format!("Delete \"{}\"?", path).as_str(),
|
"Delete this project?",
|
||||||
Some("This will delete the remote project. You can always re-add it later."),
|
Some("This will delete the remote project. You can always re-add it later."),
|
||||||
&["Delete", "Cancel"],
|
&["Delete", "Cancel"],
|
||||||
);
|
);
|
||||||
|
@ -702,12 +694,11 @@ impl DevServerProjects {
|
||||||
let dev_server_project_id = project.id;
|
let dev_server_project_id = project.id;
|
||||||
let project_id = project.project_id;
|
let project_id = project.project_id;
|
||||||
let is_online = project_id.is_some();
|
let is_online = project_id.is_some();
|
||||||
let project_path = project.path.clone();
|
|
||||||
|
|
||||||
ListItem::new(("remote-project", dev_server_project_id.0))
|
ListItem::new(("remote-project", dev_server_project_id.0))
|
||||||
.start_slot(Icon::new(IconName::FileTree).when(!is_online, |icon| icon.color(Color::Muted)))
|
.start_slot(Icon::new(IconName::FileTree).when(!is_online, |icon| icon.color(Color::Muted)))
|
||||||
.child(
|
.child(
|
||||||
Label::new(project.path.clone())
|
Label::new(project.paths.join(", "))
|
||||||
)
|
)
|
||||||
.on_click(cx.listener(move |_, _, cx| {
|
.on_click(cx.listener(move |_, _, cx| {
|
||||||
if let Some(project_id) = project_id {
|
if let Some(project_id) = project_id {
|
||||||
|
@ -723,7 +714,7 @@ impl DevServerProjects {
|
||||||
}))
|
}))
|
||||||
.end_hover_slot::<AnyElement>(Some(IconButton::new("remove-remote-project", IconName::Trash)
|
.end_hover_slot::<AnyElement>(Some(IconButton::new("remove-remote-project", IconName::Trash)
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.delete_dev_server_project(dev_server_project_id, &project_path, cx)
|
this.delete_dev_server_project(dev_server_project_id, cx)
|
||||||
}))
|
}))
|
||||||
.tooltip(|cx| Tooltip::text("Delete remote project", cx)).into_any_element()))
|
.tooltip(|cx| Tooltip::text("Delete remote project", cx)).into_any_element()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@ impl RecentProjects {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||||
workspace.register_action(|workspace, open_recent: &OpenRecent, cx| {
|
workspace.register_action(|workspace, open_recent: &OpenRecent, cx| {
|
||||||
let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
|
let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
|
||||||
Self::open(workspace, open_recent.create_new_window, cx);
|
Self::open(workspace, open_recent.create_new_window, cx);
|
||||||
|
@ -106,6 +106,20 @@ impl RecentProjects {
|
||||||
.update(cx, |picker, cx| picker.cycle_selection(cx))
|
.update(cx, |picker, cx| picker.cycle_selection(cx))
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
if workspace
|
||||||
|
.project()
|
||||||
|
.read(cx)
|
||||||
|
.dev_server_project_id()
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
workspace.register_action(|workspace, _: &workspace::Open, cx| {
|
||||||
|
if workspace.active_modal::<Self>(cx).is_some() {
|
||||||
|
cx.propagate();
|
||||||
|
} else {
|
||||||
|
Self::open(workspace, true, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open(
|
pub fn open(
|
||||||
|
@ -234,7 +248,8 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
|
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
|
||||||
format!(
|
format!(
|
||||||
"{}{}",
|
"{}{}",
|
||||||
dev_server_project.dev_server_name, dev_server_project.path
|
dev_server_project.dev_server_name,
|
||||||
|
dev_server_project.paths.join("")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -400,7 +415,8 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
|
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
|
||||||
Arc::new(vec![PathBuf::from(format!(
|
Arc::new(vec![PathBuf::from(format!(
|
||||||
"{}:{}",
|
"{}:{}",
|
||||||
dev_server_project.dev_server_name, dev_server_project.path
|
dev_server_project.dev_server_name,
|
||||||
|
dev_server_project.paths.join(", ")
|
||||||
))])
|
))])
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1465,7 +1465,7 @@ mod tests {
|
||||||
) -> (Model<Worktree>, Entry) {
|
) -> (Model<Worktree>, Entry) {
|
||||||
let (wt, _) = project
|
let (wt, _) = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree(path, true, cx)
|
project.find_or_create_worktree(path, true, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -14,6 +14,7 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
use ui::SharedString;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ use uuid::Uuid;
|
||||||
pub struct SerializedDevServerProject {
|
pub struct SerializedDevServerProject {
|
||||||
pub id: DevServerProjectId,
|
pub id: DevServerProjectId,
|
||||||
pub dev_server_name: String,
|
pub dev_server_name: String,
|
||||||
pub path: String,
|
pub paths: Vec<SharedString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
@ -119,7 +120,8 @@ impl Bind for &SerializedDevServerProject {
|
||||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
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.id.0, start_index)?;
|
||||||
let next_index = statement.bind(&self.dev_server_name, next_index)?;
|
let next_index = statement.bind(&self.dev_server_name, next_index)?;
|
||||||
statement.bind(&self.path, next_index)
|
let paths = serde_json::to_string(&self.paths)?;
|
||||||
|
statement.bind(&paths, next_index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,12 +129,18 @@ impl Column for SerializedDevServerProject {
|
||||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||||
let id = statement.column_int64(start_index)?;
|
let id = statement.column_int64(start_index)?;
|
||||||
let dev_server_name = statement.column_text(start_index + 1)?.to_string();
|
let dev_server_name = statement.column_text(start_index + 1)?.to_string();
|
||||||
let path = statement.column_text(start_index + 2)?.to_string();
|
let paths = statement.column_text(start_index + 2)?.to_string();
|
||||||
|
let paths: Vec<SharedString> = if paths.starts_with('[') {
|
||||||
|
serde_json::from_str(&paths).context("JSON deserialization of paths failed")?
|
||||||
|
} else {
|
||||||
|
vec![paths.into()]
|
||||||
|
};
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
Self {
|
Self {
|
||||||
id: DevServerProjectId(id as u64),
|
id: DevServerProjectId(id as u64),
|
||||||
dev_server_name,
|
dev_server_name,
|
||||||
path,
|
paths,
|
||||||
},
|
},
|
||||||
start_index + 3,
|
start_index + 3,
|
||||||
))
|
))
|
||||||
|
|
|
@ -53,7 +53,7 @@ pub use persistence::{
|
||||||
WorkspaceDb, DB as WORKSPACE_DB,
|
WorkspaceDb, DB as WORKSPACE_DB,
|
||||||
};
|
};
|
||||||
use postage::stream::Stream;
|
use postage::stream::Stream;
|
||||||
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use shared_screen::SharedScreen;
|
use shared_screen::SharedScreen;
|
||||||
|
@ -605,7 +605,11 @@ type PromptForNewPath = Box<
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type PromptForOpenPath = Box<
|
type PromptForOpenPath = Box<
|
||||||
dyn Fn(&mut Workspace, &mut ViewContext<Workspace>) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
|
dyn Fn(
|
||||||
|
&mut Workspace,
|
||||||
|
DirectoryLister,
|
||||||
|
&mut ViewContext<Workspace>,
|
||||||
|
) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/// Collects everything project-related for a certain window opened.
|
/// Collects everything project-related for a certain window opened.
|
||||||
|
@ -1332,13 +1336,12 @@ impl Workspace {
|
||||||
pub fn prompt_for_open_path(
|
pub fn prompt_for_open_path(
|
||||||
&mut self,
|
&mut self,
|
||||||
path_prompt_options: PathPromptOptions,
|
path_prompt_options: PathPromptOptions,
|
||||||
|
lister: DirectoryLister,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
||||||
if self.project.read(cx).is_remote()
|
if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
|
||||||
|| !WorkspaceSettings::get_global(cx).use_system_path_prompts
|
|
||||||
{
|
|
||||||
let prompt = self.on_prompt_for_open_path.take().unwrap();
|
let prompt = self.on_prompt_for_open_path.take().unwrap();
|
||||||
let rx = prompt(self, cx);
|
let rx = prompt(self, lister, cx);
|
||||||
self.on_prompt_for_open_path = Some(prompt);
|
self.on_prompt_for_open_path = Some(prompt);
|
||||||
rx
|
rx
|
||||||
} else {
|
} else {
|
||||||
|
@ -1358,7 +1361,7 @@ impl Workspace {
|
||||||
let rx = this.update(&mut cx, |this, cx| {
|
let rx = this.update(&mut cx, |this, cx| {
|
||||||
this.show_portal_error(err.to_string(), cx);
|
this.show_portal_error(err.to_string(), cx);
|
||||||
let prompt = this.on_prompt_for_open_path.take().unwrap();
|
let prompt = this.on_prompt_for_open_path.take().unwrap();
|
||||||
let rx = prompt(this, cx);
|
let rx = prompt(this, lister, cx);
|
||||||
this.on_prompt_for_open_path = Some(prompt);
|
this.on_prompt_for_open_path = Some(prompt);
|
||||||
rx
|
rx
|
||||||
})?;
|
})?;
|
||||||
|
@ -1419,7 +1422,7 @@ impl Workspace {
|
||||||
let project_path = abs_path.and_then(|abs_path| {
|
let project_path = abs_path.and_then(|abs_path| {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.project.update(cx, |project, cx| {
|
this.project.update(cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree(abs_path, true, cx)
|
project.find_or_create_worktree(abs_path, true, cx)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.ok()
|
.ok()
|
||||||
|
@ -1703,6 +1706,7 @@ impl Workspace {
|
||||||
directories: true,
|
directories: true,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
},
|
},
|
||||||
|
DirectoryLister::Local(self.app_state.fs.clone()),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1857,9 +1861,10 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
|
fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
|
||||||
if self.project.read(cx).is_remote() {
|
let project = self.project.read(cx);
|
||||||
|
if project.is_remote() && project.dev_server_project_id().is_none() {
|
||||||
self.show_error(
|
self.show_error(
|
||||||
&anyhow!("Folders cannot yet be added to remote projects"),
|
&anyhow!("You cannot add folders to someone else's project"),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -1870,6 +1875,7 @@ impl Workspace {
|
||||||
directories: true,
|
directories: true,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
},
|
},
|
||||||
|
DirectoryLister::Project(self.project.clone()),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
@ -1895,7 +1901,7 @@ impl Workspace {
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Task<Result<(Model<Worktree>, ProjectPath)>> {
|
) -> Task<Result<(Model<Worktree>, ProjectPath)>> {
|
||||||
let entry = project.update(cx, |project, cx| {
|
let entry = project.update(cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree(abs_path, visible, cx)
|
project.find_or_create_worktree(abs_path, visible, cx)
|
||||||
});
|
});
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let (worktree, path) = entry.await?;
|
let (worktree, path) = entry.await?;
|
||||||
|
@ -3852,7 +3858,7 @@ impl Workspace {
|
||||||
let dev_server_project = SerializedDevServerProject {
|
let dev_server_project = SerializedDevServerProject {
|
||||||
id: dev_server_project_id,
|
id: dev_server_project_id,
|
||||||
dev_server_name: dev_server.name.to_string(),
|
dev_server_name: dev_server.name.to_string(),
|
||||||
path: project.path.to_string(),
|
paths: project.paths.iter().map(|path| path.clone()).collect(),
|
||||||
};
|
};
|
||||||
Some(SerializedWorkspaceLocation::DevServer(dev_server_project))
|
Some(SerializedWorkspaceLocation::DevServer(dev_server_project))
|
||||||
})
|
})
|
||||||
|
@ -3978,6 +3984,9 @@ impl Workspace {
|
||||||
.on_action(cx.listener(Self::send_keystrokes))
|
.on_action(cx.listener(Self::send_keystrokes))
|
||||||
.on_action(cx.listener(Self::add_folder_to_project))
|
.on_action(cx.listener(Self::add_folder_to_project))
|
||||||
.on_action(cx.listener(Self::follow_next_collaborator))
|
.on_action(cx.listener(Self::follow_next_collaborator))
|
||||||
|
.on_action(cx.listener(Self::open))
|
||||||
|
.on_action(cx.listener(Self::close_window))
|
||||||
|
.on_action(cx.listener(Self::activate_pane_at_index))
|
||||||
.on_action(cx.listener(|workspace, _: &Unfollow, cx| {
|
.on_action(cx.listener(|workspace, _: &Unfollow, cx| {
|
||||||
let pane = workspace.active_pane().clone();
|
let pane = workspace.active_pane().clone();
|
||||||
workspace.unfollow_in_pane(&pane, cx);
|
workspace.unfollow_in_pane(&pane, cx);
|
||||||
|
@ -4034,9 +4043,6 @@ impl Workspace {
|
||||||
workspace.clear_all_notifications(cx);
|
workspace.clear_all_notifications(cx);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.on_action(cx.listener(Workspace::open))
|
|
||||||
.on_action(cx.listener(Workspace::close_window))
|
|
||||||
.on_action(cx.listener(Workspace::activate_pane_at_index))
|
|
||||||
.on_action(
|
.on_action(
|
||||||
cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
|
cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
|
||||||
workspace.reopen_closed_item(cx).detach();
|
workspace.reopen_closed_item(cx).detach();
|
||||||
|
@ -4083,13 +4089,7 @@ impl Workspace {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
|
fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
|
||||||
let mut div = div
|
|
||||||
.on_action(cx.listener(Self::close_inactive_items_and_panes))
|
|
||||||
.on_action(cx.listener(Self::close_all_items_and_panes))
|
|
||||||
.on_action(cx.listener(Self::add_folder_to_project))
|
|
||||||
.on_action(cx.listener(Self::save_all))
|
|
||||||
.on_action(cx.listener(Self::open));
|
|
||||||
for action in self.workspace_actions.iter() {
|
for action in self.workspace_actions.iter() {
|
||||||
div = (action)(div, cx)
|
div = (action)(div, cx)
|
||||||
}
|
}
|
||||||
|
@ -5506,7 +5506,7 @@ mod tests {
|
||||||
// Add a project folder
|
// Add a project folder
|
||||||
project
|
project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree("root2", true, cx)
|
project.find_or_create_worktree("root2", true, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -942,7 +942,7 @@ fn open_settings_file(
|
||||||
let worktree_creation_task = workspace.project().update(cx, |project, cx| {
|
let worktree_creation_task = workspace.project().update(cx, |project, cx| {
|
||||||
// Set up a dedicated worktree for settings, since otherwise we're dropping and re-starting LSP servers for each file inside on every settings file close/open
|
// Set up a dedicated worktree for settings, since otherwise we're dropping and re-starting LSP servers for each file inside on every settings file close/open
|
||||||
// TODO: Do note that all other external files (e.g. drag and drop from OS) still have their worktrees released on file close, causing LSP servers' restarts.
|
// TODO: Do note that all other external files (e.g. drag and drop from OS) still have their worktrees released on file close, causing LSP servers' restarts.
|
||||||
project.find_or_create_local_worktree(paths::config_dir().as_path(), false, cx)
|
project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
|
||||||
});
|
});
|
||||||
let settings_open_task = create_and_open_local_file(&abs_path, cx, default_content);
|
let settings_open_task = create_and_open_local_file(&abs_path, cx, default_content);
|
||||||
(worktree_creation_task, settings_open_task)
|
(worktree_creation_task, settings_open_task)
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
# Remote Development
|
# Remote Development
|
||||||
|
|
||||||
Remote Development is in the early stages of development. If you'd like to try it please email [alpha@zed.dev](mailto:alpha@zed.dev).
|
|
||||||
|
|
||||||
Remote Development allows you to code at the speed of thought, even when your codebase is not on your local machine. You use Zed locally so the UI is immediately responsive, but offload heavy computation to the development server so that you can work effectively.
|
Remote Development allows you to code at the speed of thought, even when your codebase is not on your local machine. You use Zed locally so the UI is immediately responsive, but offload heavy computation to the development server so that you can work effectively.
|
||||||
|
|
||||||
|
> **Note:** Remoting is still "alpha". We have several changes we would like to make before it is fully released.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Remote development requires running two instances of Zed. A headless instance on the remote machine, and the editor interface on your local computer. All configuration is done on your local computer.
|
Remote development requires running two instances of Zed. A headless instance on the remote machine, and the editor interface on your local computer. All configuration is done on your local computer.
|
||||||
|
|
||||||
Currently the two instances connect via Zed's servers, but we intend to build peer to peer communication in the future.
|
Currently the two instances connect via Zed's servers, but we intend to build peer to peer communication before the feature is fully released.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
> **Note:** You must be in the alpha program to see this UI. The instructions will likely change as the feature gets closer to launch.
|
|
||||||
|
|
||||||
1. Download and install the latest [Zed Preview](https://zed.dev/releases/preview).
|
1. Download and install the latest [Zed Preview](https://zed.dev/releases/preview).
|
||||||
1. Open the remote projects dialogue with `cmd-shift-p remote`.
|
1. Open the remote projects dialogue with `cmd-shift-p remote`.
|
||||||
2. Click "New Server".
|
2. Click "New Server".
|
||||||
|
@ -27,7 +25,7 @@ Currently the two instances connect via Zed's servers, but we intend to build pe
|
||||||
|
|
||||||
### UI is not showing up
|
### UI is not showing up
|
||||||
|
|
||||||
This can happen either if you were just added to the alpha, in which case you need to restart Zed. Or, if you lost connection to the Zed server, in which case you just need to click "Sign In" in the top right.
|
You need to be on a relatively recent Zed (v0.145.0 or later).
|
||||||
|
|
||||||
### SSH connections
|
### SSH connections
|
||||||
|
|
||||||
|
@ -86,8 +84,8 @@ If you'd like to install language-server extensions, you can add them to the lis
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
- You can't use the Terminal or Tasks if you choose "Manual Connection"
|
- You can't use the Terminal or Tasks if you choose "Manual Connection"
|
||||||
- You can't yet open additional files on the machine in the current project.
|
|
||||||
- You can't run `zed` in headless mode and in GUI mode at the same time on the same machine.
|
- You can't run `zed` in headless mode and in GUI mode at the same time on the same machine.
|
||||||
|
- You can't open files from the remote Terminal by typing the `zed` command.
|
||||||
|
|
||||||
## Feedback
|
## Feedback
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue