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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
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::{
|
||||
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
|
||||
ModelTrait, QueryFilter,
|
||||
IntoActiveModel, ModelTrait, QueryFilter,
|
||||
};
|
||||
|
||||
use crate::db::ProjectId;
|
||||
|
@ -56,12 +56,7 @@ impl Database {
|
|||
.await?;
|
||||
Ok(servers
|
||||
.into_iter()
|
||||
.map(|(dev_server_project, project)| proto::DevServerProject {
|
||||
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,
|
||||
})
|
||||
.map(|(dev_server_project, project)| dev_server_project.to_proto(project))
|
||||
.collect())
|
||||
}
|
||||
|
||||
|
@ -134,7 +129,7 @@ impl Database {
|
|||
let project = dev_server_project::Entity::insert(dev_server_project::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
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)
|
||||
.await?;
|
||||
|
@ -148,6 +143,38 @@ impl Database {
|
|||
.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(
|
||||
&self,
|
||||
dev_server_project_id: DevServerProjectId,
|
||||
|
@ -258,7 +285,6 @@ impl Database {
|
|||
dev_server_id: DevServerId,
|
||||
connection: ConnectionId,
|
||||
) -> 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 {
|
||||
let mut ret = Vec::new();
|
||||
for reshared_project in reshared_projects {
|
||||
|
@ -322,7 +348,6 @@ impl Database {
|
|||
user_id: UserId,
|
||||
connection_id: ConnectionId,
|
||||
) -> 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 {
|
||||
let mut ret = Vec::new();
|
||||
for rejoined_project in rejoined_projects {
|
||||
|
|
|
@ -19,6 +19,28 @@ impl Database {
|
|||
.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>> {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(dev_server::Entity::find()
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use super::project;
|
||||
use crate::db::{DevServerId, DevServerProjectId};
|
||||
use rpc::proto;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{entity::prelude::*, FromJsonQueryResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "dev_server_projects")]
|
||||
|
@ -9,9 +10,12 @@ pub struct Model {
|
|||
#[sea_orm(primary_key)]
|
||||
pub id: DevServerProjectId,
|
||||
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 {}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
@ -44,7 +48,12 @@ impl Model {
|
|||
id: self.id.to_proto(),
|
||||
project_id: project.map(|p| p.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(rejoin_dev_server_projects))
|
||||
.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(create_dev_server))
|
||||
.add_request_handler(user_handler(regenerate_dev_server_token))
|
||||
.add_request_handler(user_handler(rename_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(shutdown_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)
|
||||
}
|
||||
|
||||
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(
|
||||
request: proto::CreateDevServerProject,
|
||||
response: Response<proto::CreateDevServerProject>,
|
||||
|
|
|
@ -38,6 +38,10 @@ impl ZedVersion {
|
|||
pub fn with_save_as() -> ZedVersion {
|
||||
ZedVersion(SemanticVersion::new(0, 134, 0))
|
||||
}
|
||||
|
||||
pub fn with_list_directory() -> ZedVersion {
|
||||
ZedVersion(SemanticVersion::new(0, 145, 0))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait VersionedMessage {
|
||||
|
@ -187,6 +191,18 @@ impl ConnectionPool {
|
|||
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(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
|
|
@ -66,7 +66,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
|||
.update(cx, |store, cx| {
|
||||
let projects = store.dev_server_projects();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].path, "/remote");
|
||||
assert_eq!(projects[0].paths, vec!["/remote"]);
|
||||
workspace::join_dev_server_project(
|
||||
projects[0].id,
|
||||
projects[0].project_id.unwrap(),
|
||||
|
@ -206,7 +206,7 @@ async fn create_dev_server_project(
|
|||
.update(cx, |store, cx| {
|
||||
let projects = store.dev_server_projects();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].path, "/remote");
|
||||
assert_eq!(projects[0].paths, vec!["/remote"]);
|
||||
workspace::join_dev_server_project(
|
||||
projects[0].id,
|
||||
projects[0].project_id.unwrap(),
|
||||
|
|
|
@ -1528,7 +1528,7 @@ async fn test_project_reconnect(
|
|||
});
|
||||
let (worktree_a2, _) = project_a1
|
||||
.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
|
||||
.unwrap();
|
||||
|
@ -1601,7 +1601,7 @@ async fn test_project_reconnect(
|
|||
});
|
||||
let (worktree_a3, _) = project_a1
|
||||
.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
|
||||
.unwrap();
|
||||
|
@ -1725,7 +1725,7 @@ async fn test_project_reconnect(
|
|||
// While client B is disconnected, add and remove worktrees from client A's project.
|
||||
let (worktree_a4, _) = project_a1
|
||||
.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
|
||||
.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 (worktree_2, _) = project_a
|
||||
.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
|
||||
.unwrap();
|
||||
|
|
|
@ -581,7 +581,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
|||
}
|
||||
project
|
||||
.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
|
||||
.unwrap();
|
||||
|
|
|
@ -805,9 +805,7 @@ impl TestClient {
|
|||
) -> (Model<Project>, WorktreeId) {
|
||||
let project = self.build_empty_local_project(cx);
|
||||
let (worktree, _) = project
|
||||
.update(cx, |p, cx| {
|
||||
p.find_or_create_local_worktree(root_path, true, cx)
|
||||
})
|
||||
.update(cx, |p, cx| p.find_or_create_worktree(root_path, true, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
worktree
|
||||
|
|
|
@ -20,7 +20,7 @@ pub struct Store {
|
|||
pub struct DevServerProject {
|
||||
pub id: DevServerProjectId,
|
||||
pub project_id: Option<ProjectId>,
|
||||
pub path: SharedString,
|
||||
pub paths: Vec<SharedString>,
|
||||
pub dev_server_id: DevServerId,
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ impl From<proto::DevServerProject> for DevServerProject {
|
|||
Self {
|
||||
id: DevServerProjectId(project.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),
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ impl Store {
|
|||
.filter(|project| project.dev_server_id == id)
|
||||
.cloned()
|
||||
.collect();
|
||||
projects.sort_by_key(|p| (p.path.clone(), p.id));
|
||||
projects.sort_by_key(|p| (p.paths.clone(), p.id));
|
||||
projects
|
||||
}
|
||||
|
||||
|
@ -108,7 +108,7 @@ impl Store {
|
|||
pub fn dev_server_projects(&self) -> Vec<DevServerProject> {
|
||||
let mut projects: Vec<DevServerProject> =
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -935,7 +935,7 @@ impl Item for Editor {
|
|||
.context("No path stored for this editor")?;
|
||||
|
||||
let (worktree, path) = project
|
||||
.find_local_worktree(&path, cx)
|
||||
.find_worktree(&path, cx)
|
||||
.with_context(|| format!("No worktree for path: {path:?}"))?;
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
|
|
|
@ -79,7 +79,7 @@ impl EditorLspTestContext {
|
|||
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
|
||||
project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root", true, cx)
|
||||
project.find_or_create_worktree("/root", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -19,6 +19,7 @@ use gpui::{
|
|||
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
use num_format::{Locale, ToFormattedString};
|
||||
use project::DirectoryLister;
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::Settings;
|
||||
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)
|
||||
}
|
||||
})
|
||||
.register_action(move |_, _: &InstallDevExtension, cx| {
|
||||
.register_action(move |workspace, _: &InstallDevExtension, cx| {
|
||||
let store = ExtensionStore::global(cx);
|
||||
let prompt = cx.prompt_for_paths(gpui::PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: false,
|
||||
});
|
||||
let prompt = workspace.prompt_for_open_path(
|
||||
gpui::PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: false,
|
||||
},
|
||||
DirectoryLister::Local(workspace.app_state().fs.clone()),
|
||||
cx,
|
||||
);
|
||||
|
||||
let workspace_handle = cx.view().downgrade();
|
||||
cx.deref_mut()
|
||||
|
|
|
@ -679,7 +679,7 @@ impl FileFinderDelegate {
|
|||
let update_result = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
if let Some((worktree, relative_path)) =
|
||||
project.find_local_worktree(query_path, cx)
|
||||
project.find_worktree(query_path, cx)
|
||||
{
|
||||
path_matches.push(ProjectPanelOrdMatch(PathMatch {
|
||||
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;
|
||||
cx.update(|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();
|
||||
|
@ -1513,7 +1513,7 @@ async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
|
|||
project
|
||||
.update(cx, |project, cx| {
|
||||
project
|
||||
.find_or_create_local_worktree("/test/project_2", true, cx)
|
||||
.find_or_create_worktree("/test/project_2", true, cx)
|
||||
.into_future()
|
||||
})
|
||||
.await
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
use futures::channel::oneshot;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::Model;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{compare_paths, Project};
|
||||
use project::{compare_paths, DirectoryLister};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
|
@ -19,7 +18,7 @@ pub(crate) struct OpenPathPrompt;
|
|||
|
||||
pub struct OpenPathDelegate {
|
||||
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
|
||||
project: Model<Project>,
|
||||
lister: DirectoryLister,
|
||||
selected_index: usize,
|
||||
directory_state: Option<DirectoryState>,
|
||||
matches: Vec<usize>,
|
||||
|
@ -35,23 +34,23 @@ struct DirectoryState {
|
|||
|
||||
impl OpenPathPrompt {
|
||||
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();
|
||||
Self::prompt_for_open_path(workspace, tx, cx);
|
||||
Self::prompt_for_open_path(workspace, lister, tx, cx);
|
||||
rx
|
||||
}));
|
||||
}
|
||||
|
||||
fn prompt_for_open_path(
|
||||
workspace: &mut Workspace,
|
||||
lister: DirectoryLister,
|
||||
tx: oneshot::Sender<Option<Vec<PathBuf>>>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let project = workspace.project().clone();
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
let delegate = OpenPathDelegate {
|
||||
tx: Some(tx),
|
||||
project: project.clone(),
|
||||
lister: lister.clone(),
|
||||
selected_index: 0,
|
||||
directory_state: None,
|
||||
matches: Vec::new(),
|
||||
|
@ -60,11 +59,7 @@ impl OpenPathPrompt {
|
|||
};
|
||||
|
||||
let picker = Picker::uniform_list(delegate, cx).width(rems(34.));
|
||||
let query = if let Some(worktree) = project.read(cx).visible_worktrees(cx).next() {
|
||||
worktree.read(cx).abs_path().to_string_lossy().to_string()
|
||||
} else {
|
||||
"~/".to_string()
|
||||
};
|
||||
let query = lister.default_query(cx);
|
||||
picker.set_query(query, cx);
|
||||
picker
|
||||
});
|
||||
|
@ -92,7 +87,7 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
query: String,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let project = self.project.clone();
|
||||
let lister = self.lister.clone();
|
||||
let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
|
||||
(query[..index].to_string(), query[index + 1..].to_string())
|
||||
} else {
|
||||
|
@ -109,9 +104,7 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
{
|
||||
None
|
||||
} else {
|
||||
Some(project.update(cx, |project, cx| {
|
||||
project.completions_for_open_path_query(dir.clone(), cx)
|
||||
}))
|
||||
Some(lister.list_directory(dir.clone(), cx))
|
||||
};
|
||||
self.cancel_flag.store(true, atomic::Ordering::Relaxed);
|
||||
self.cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
|
@ -127,20 +120,12 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
this.update(&mut cx, |this, _| {
|
||||
this.delegate.directory_state = Some(match paths {
|
||||
Ok(mut paths) => {
|
||||
paths.sort_by(|a, b| {
|
||||
compare_paths(
|
||||
(a.strip_prefix(&dir).unwrap_or(Path::new("")), true),
|
||||
(b.strip_prefix(&dir).unwrap_or(Path::new("")), true),
|
||||
)
|
||||
});
|
||||
paths.sort_by(|a, b| compare_paths((a, true), (b, true)));
|
||||
let match_candidates = paths
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ix, path)| {
|
||||
Some(StringMatchCandidate::new(
|
||||
ix,
|
||||
path.file_name()?.to_string_lossy().into(),
|
||||
))
|
||||
.map(|(ix, path)| {
|
||||
StringMatchCandidate::new(ix, path.to_string_lossy().into())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
@ -213,7 +198,16 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
this.delegate
|
||||
.matches
|
||||
.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();
|
||||
})
|
||||
.ok();
|
||||
|
|
|
@ -3,7 +3,7 @@ use client::DevServerProjectId;
|
|||
use client::{user::UserStore, Client, ClientSettings};
|
||||
use extension::ExtensionStore;
|
||||
use fs::Fs;
|
||||
use futures::Future;
|
||||
use futures::{Future, StreamExt};
|
||||
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, Task, WeakModel};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::NodeRuntime;
|
||||
|
@ -11,6 +11,7 @@ use postage::stream::Stream;
|
|||
use project::Project;
|
||||
use rpc::{proto, ErrorCode, TypedEnvelope};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::path::Path;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
|
@ -96,6 +97,7 @@ impl DevServer {
|
|||
cx.weak_model(),
|
||||
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),
|
||||
],
|
||||
_maintain_connection: maintain_connection,
|
||||
|
@ -127,34 +129,43 @@ impl DevServer {
|
|||
envelope: TypedEnvelope<proto::DevServerInstructions>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let (added_projects, removed_projects_ids) = this.read_with(&mut cx, |this, _| {
|
||||
let removed_projects = this
|
||||
.projects
|
||||
.keys()
|
||||
.filter(|dev_server_project_id| {
|
||||
!envelope
|
||||
.payload
|
||||
.projects
|
||||
.iter()
|
||||
.any(|p| p.id == dev_server_project_id.0)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let (added_projects, retained_projects, removed_projects_ids) =
|
||||
this.read_with(&mut cx, |this, _| {
|
||||
let removed_projects = this
|
||||
.projects
|
||||
.keys()
|
||||
.filter(|dev_server_project_id| {
|
||||
!envelope
|
||||
.payload
|
||||
.projects
|
||||
.iter()
|
||||
.any(|p| p.id == dev_server_project_id.0)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let added_projects = envelope
|
||||
.payload
|
||||
.projects
|
||||
.into_iter()
|
||||
.filter(|project| !this.projects.contains_key(&DevServerProjectId(project.id)))
|
||||
.collect::<Vec<_>>();
|
||||
let mut added_projects = vec![];
|
||||
let mut retained_projects = 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 {
|
||||
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| {
|
||||
for old_project_id in &removed_projects_ids {
|
||||
this.unshare_project(old_project_id, cx)?;
|
||||
|
@ -181,6 +192,24 @@ impl DevServer {
|
|||
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(
|
||||
this: Model<Self>,
|
||||
_envelope: TypedEnvelope<proto::ShutdownDevServer>,
|
||||
|
@ -221,17 +250,19 @@ impl DevServer {
|
|||
(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
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree(&path, true, cx)
|
||||
})?
|
||||
.await?;
|
||||
let (worktree, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree(&path, true, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
worktree.update(cx, |worktree, cx| {
|
||||
worktree.as_local_mut().unwrap().share_private_files(cx)
|
||||
})?;
|
||||
worktree.update(cx, |worktree, cx| {
|
||||
worktree.as_local_mut().unwrap().share_private_files(cx)
|
||||
})?;
|
||||
}
|
||||
|
||||
let worktrees =
|
||||
project.read_with(cx, |project, cx| project.worktree_metadata_protos(cx))?;
|
||||
|
@ -252,6 +283,56 @@ impl DevServer {
|
|||
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(
|
||||
this: WeakModel<Self>,
|
||||
client: Arc<Client>,
|
||||
|
|
|
@ -29,7 +29,7 @@ use futures::{
|
|||
future::{join_all, try_join_all, Shared},
|
||||
select,
|
||||
stream::FuturesUnordered,
|
||||
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
||||
AsyncWriteExt, Future, FutureExt, StreamExt,
|
||||
};
|
||||
use fuzzy::CharBag;
|
||||
use git::{blame::Blame, repository::GitRepository};
|
||||
|
@ -86,6 +86,7 @@ use snippet::Snippet;
|
|||
use snippet_provider::SnippetProvider;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
cmp::{self, Ordering},
|
||||
convert::TryInto,
|
||||
env,
|
||||
|
@ -202,7 +203,7 @@ pub struct Project {
|
|||
_subscriptions: Vec<gpui::Subscription>,
|
||||
shared_buffers: HashMap<proto::PeerId, HashSet<BufferId>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
loading_local_worktrees:
|
||||
loading_worktrees:
|
||||
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
|
||||
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)]
|
||||
enum SearchMatchCandidate {
|
||||
OpenBuffer {
|
||||
|
@ -727,7 +774,7 @@ impl Project {
|
|||
collaborators: Default::default(),
|
||||
buffer_store,
|
||||
shared_buffers: Default::default(),
|
||||
loading_local_worktrees: Default::default(),
|
||||
loading_worktrees: Default::default(),
|
||||
buffer_snapshots: Default::default(),
|
||||
join_project_response_message_id: 0,
|
||||
client_state: ProjectClientState::Local,
|
||||
|
@ -866,7 +913,7 @@ impl Project {
|
|||
buffer_ordered_messages_tx: tx,
|
||||
buffer_store,
|
||||
shared_buffers: Default::default(),
|
||||
loading_local_worktrees: Default::default(),
|
||||
loading_worktrees: Default::default(),
|
||||
active_entry: None,
|
||||
collaborators: Default::default(),
|
||||
join_project_response_message_id: response.message_id,
|
||||
|
@ -1068,7 +1115,7 @@ impl Project {
|
|||
for path in root_paths {
|
||||
let (tree, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree(path, true, cx)
|
||||
project.find_or_create_worktree(path, true, cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
|
@ -1106,7 +1153,7 @@ impl Project {
|
|||
for path in root_paths {
|
||||
let (tree, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree(path, true, cx)
|
||||
project.find_or_create_worktree(path, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -1909,7 +1956,7 @@ impl Project {
|
|||
abs_path: impl AsRef<Path>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> 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)
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no such path")))
|
||||
|
@ -1976,7 +2023,7 @@ impl Project {
|
|||
};
|
||||
let (worktree, relative_path) = if let Some(result) = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.find_local_worktree(&worktree_root_target, cx)
|
||||
this.find_worktree(&worktree_root_target, cx)
|
||||
})? {
|
||||
let relative_path =
|
||||
known_relative_path.unwrap_or_else(|| Arc::<Path>::from(result.1));
|
||||
|
@ -1984,7 +2031,7 @@ impl Project {
|
|||
} else {
|
||||
let worktree = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.create_local_worktree(&worktree_root_target, false, cx)
|
||||
this.create_worktree(&worktree_root_target, false, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
|
@ -4572,7 +4619,7 @@ impl Project {
|
|||
cx: &mut ModelContext<Project>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
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:?}"))?;
|
||||
|
||||
let project_path = ProjectPath {
|
||||
|
@ -5440,9 +5487,7 @@ impl Project {
|
|||
|
||||
let path;
|
||||
let worktree;
|
||||
if let Some((tree, rel_path)) =
|
||||
this.find_local_worktree(&abs_path, cx)
|
||||
{
|
||||
if let Some((tree, rel_path)) = this.find_worktree(&abs_path, cx) {
|
||||
worktree = tree;
|
||||
path = rel_path;
|
||||
} else {
|
||||
|
@ -7516,23 +7561,23 @@ impl Project {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn find_or_create_local_worktree(
|
||||
pub fn find_or_create_worktree(
|
||||
&mut self,
|
||||
abs_path: impl AsRef<Path>,
|
||||
visible: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<(Model<Worktree>, PathBuf)>> {
|
||||
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)))
|
||||
} else {
|
||||
let worktree = self.create_local_worktree(abs_path, visible, cx);
|
||||
let worktree = self.create_worktree(abs_path, visible, cx);
|
||||
cx.background_executor()
|
||||
.spawn(async move { Ok((worktree.await?, PathBuf::new())) })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_local_worktree(
|
||||
pub fn find_worktree(
|
||||
&self,
|
||||
abs_path: &Path,
|
||||
cx: &AppContext,
|
||||
|
@ -7559,21 +7604,56 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn completions_for_open_path_query(
|
||||
pub fn list_directory(
|
||||
&self,
|
||||
query: String,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> 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 {
|
||||
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 {
|
||||
results.push(path?);
|
||||
}
|
||||
Ok(results)
|
||||
let result = match task.await {
|
||||
Ok(worktree) => Ok(worktree),
|
||||
Err(err) => Err(anyhow!("{}", err)),
|
||||
};
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -7582,51 +7662,102 @@ impl Project {
|
|||
abs_path: impl AsRef<Path>,
|
||||
visible: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<Worktree>>> {
|
||||
) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
|
||||
let fs = self.fs.clone();
|
||||
let next_entry_id = self.next_entry_id.clone();
|
||||
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, _| {
|
||||
project.loading_local_worktrees.remove(&path);
|
||||
})?;
|
||||
cx.spawn(move |project, mut cx| async move {
|
||||
let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await;
|
||||
|
||||
let worktree = worktree?;
|
||||
project
|
||||
.update(&mut cx, |project, cx| project.add_worktree(&worktree, cx))?;
|
||||
project.update(&mut cx, |project, _| {
|
||||
project.loading_worktrees.remove(&path);
|
||||
})?;
|
||||
|
||||
if visible {
|
||||
cx.update(|cx| {
|
||||
cx.add_recent_document(&path);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
let worktree = worktree?;
|
||||
project.update(&mut cx, |project, cx| project.add_worktree(&worktree, cx))?;
|
||||
|
||||
Ok(worktree)
|
||||
}
|
||||
.map_err(Arc::new)
|
||||
if visible {
|
||||
cx.update(|cx| {
|
||||
cx.add_recent_document(&path);
|
||||
})
|
||||
.shared()
|
||||
})
|
||||
.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
match task.await {
|
||||
Ok(worktree) => Ok(worktree),
|
||||
Err(err) => Err(anyhow!("{}", err)),
|
||||
.log_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>) {
|
||||
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.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(
|
||||
&self,
|
||||
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 (worktree, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root/dir", true, cx)
|
||||
project.find_or_create_worktree("/root/dir", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -995,7 +995,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
|
|||
|
||||
let (worktree, _) = project
|
||||
.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
|
||||
.unwrap();
|
||||
|
|
|
@ -55,7 +55,9 @@ impl Project {
|
|||
} else {
|
||||
projects_store
|
||||
.dev_server_project(dev_server_project_id)?
|
||||
.path
|
||||
.paths
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
|
@ -81,8 +83,8 @@ impl Project {
|
|||
.and_then(|cwd| cwd.local_path());
|
||||
|
||||
terminal_cwd
|
||||
.and_then(|terminal_cwd| self.find_local_worktree(&terminal_cwd, cx))
|
||||
.or_else(|| task_cwd.and_then(|spawn_cwd| self.find_local_worktree(&spawn_cwd, cx)))
|
||||
.and_then(|terminal_cwd| self.find_worktree(&terminal_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 {
|
||||
|
|
|
@ -460,9 +460,8 @@ impl ProjectPanel {
|
|||
let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
|
||||
let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
|
||||
let worktree_id = worktree.id();
|
||||
let is_local = project.is_local();
|
||||
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| {
|
||||
menu.context(self.focus_handle.clone()).when_else(
|
||||
|
@ -526,14 +525,12 @@ impl ProjectPanel {
|
|||
menu.action("Trash", Box::new(Trash { 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()
|
||||
.when(!is_remote, |menu| {
|
||||
menu.action(
|
||||
"Add Folder to Project…",
|
||||
Box::new(workspace::AddFolderToProject),
|
||||
)
|
||||
})
|
||||
.action(
|
||||
"Add Folder to Project…",
|
||||
Box::new(workspace::AddFolderToProject),
|
||||
)
|
||||
.entry(
|
||||
"Remove from Project",
|
||||
None,
|
||||
|
@ -544,7 +541,7 @@ impl ProjectPanel {
|
|||
}),
|
||||
)
|
||||
})
|
||||
.when(is_local & is_root, |menu| {
|
||||
.when(is_root, |menu| {
|
||||
menu.separator()
|
||||
.action("Collapse All", Box::new(CollapseAllEntries))
|
||||
})
|
||||
|
|
|
@ -265,7 +265,10 @@ message Envelope {
|
|||
SynchronizeContextsResponse synchronize_contexts_response = 216;
|
||||
|
||||
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;
|
||||
|
@ -507,6 +510,20 @@ message ValidateDevServerProjectRequest {
|
|||
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 {
|
||||
reserved 1;
|
||||
string name = 2;
|
||||
|
@ -1335,6 +1352,7 @@ message DevServerProject {
|
|||
reserved 4;
|
||||
uint64 dev_server_id = 5;
|
||||
string path = 6;
|
||||
repeated string paths = 7;
|
||||
}
|
||||
|
||||
message DevServer {
|
||||
|
|
|
@ -377,6 +377,9 @@ messages!(
|
|||
(MultiLspQueryResponse, Background),
|
||||
(DevServerProjectsUpdate, Foreground),
|
||||
(ValidateDevServerProjectRequest, Background),
|
||||
(ListRemoteDirectory, Background),
|
||||
(ListRemoteDirectoryResponse, Background),
|
||||
(UpdateDevServerProject, Background),
|
||||
(DeleteDevServer, Foreground),
|
||||
(DeleteDevServerProject, Foreground),
|
||||
(RegenerateDevServerToken, Foreground),
|
||||
|
@ -434,6 +437,8 @@ request_messages!(
|
|||
(GetSupermavenApiKey, GetSupermavenApiKeyResponse),
|
||||
(GetTypeDefinition, GetTypeDefinitionResponse),
|
||||
(LinkedEditingRange, LinkedEditingRangeResponse),
|
||||
(ListRemoteDirectory, ListRemoteDirectoryResponse),
|
||||
(UpdateDevServerProject, Ack),
|
||||
(GetUsers, UsersResponse),
|
||||
(IncomingCall, Ack),
|
||||
(InlayHints, InlayHintsResponse),
|
||||
|
|
|
@ -173,16 +173,13 @@ impl DevServerProjects {
|
|||
.read(cx)
|
||||
.projects_for_server(dev_server_id)
|
||||
.iter()
|
||||
.any(|p| p.path == path)
|
||||
.any(|p| p.paths.iter().any(|p| p == &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
|
||||
)),
|
||||
Some(&format!("{} is already open on this dev server.", path)),
|
||||
&["Ok"],
|
||||
)
|
||||
.await
|
||||
|
@ -454,15 +451,10 @@ impl DevServerProjects {
|
|||
.detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None);
|
||||
}
|
||||
|
||||
fn delete_dev_server_project(
|
||||
&mut self,
|
||||
id: DevServerProjectId,
|
||||
path: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
fn delete_dev_server_project(&mut self, id: DevServerProjectId, cx: &mut ViewContext<Self>) {
|
||||
let answer = cx.prompt(
|
||||
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."),
|
||||
&["Delete", "Cancel"],
|
||||
);
|
||||
|
@ -702,12 +694,11 @@ impl DevServerProjects {
|
|||
let dev_server_project_id = project.id;
|
||||
let project_id = project.project_id;
|
||||
let is_online = project_id.is_some();
|
||||
let project_path = project.path.clone();
|
||||
|
||||
ListItem::new(("remote-project", dev_server_project_id.0))
|
||||
.start_slot(Icon::new(IconName::FileTree).when(!is_online, |icon| icon.color(Color::Muted)))
|
||||
.child(
|
||||
Label::new(project.path.clone())
|
||||
Label::new(project.paths.join(", "))
|
||||
)
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
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)
|
||||
.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()))
|
||||
}
|
||||
|
|
|
@ -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| {
|
||||
let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
|
||||
Self::open(workspace, open_recent.create_new_window, cx);
|
||||
|
@ -106,6 +106,20 @@ impl RecentProjects {
|
|||
.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(
|
||||
|
@ -234,7 +248,8 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
|
||||
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) => {
|
||||
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) {
|
||||
let (wt, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree(path, true, cx)
|
||||
project.find_or_create_worktree(path, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -14,6 +14,7 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::SharedString;
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -21,7 +22,7 @@ use uuid::Uuid;
|
|||
pub struct SerializedDevServerProject {
|
||||
pub id: DevServerProjectId,
|
||||
pub dev_server_name: String,
|
||||
pub path: String,
|
||||
pub paths: Vec<SharedString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
|
@ -119,7 +120,8 @@ impl Bind for &SerializedDevServerProject {
|
|||
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)
|
||||
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)> {
|
||||
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();
|
||||
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((
|
||||
Self {
|
||||
id: DevServerProjectId(id as u64),
|
||||
dev_server_name,
|
||||
path,
|
||||
paths,
|
||||
},
|
||||
start_index + 3,
|
||||
))
|
||||
|
|
|
@ -53,7 +53,7 @@ pub use persistence::{
|
|||
WorkspaceDb, DB as WORKSPACE_DB,
|
||||
};
|
||||
use postage::stream::Stream;
|
||||
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||
use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use shared_screen::SharedScreen;
|
||||
|
@ -605,7 +605,11 @@ type PromptForNewPath = 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.
|
||||
|
@ -1332,13 +1336,12 @@ impl Workspace {
|
|||
pub fn prompt_for_open_path(
|
||||
&mut self,
|
||||
path_prompt_options: PathPromptOptions,
|
||||
lister: DirectoryLister,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
||||
if self.project.read(cx).is_remote()
|
||||
|| !WorkspaceSettings::get_global(cx).use_system_path_prompts
|
||||
{
|
||||
if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
|
||||
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);
|
||||
rx
|
||||
} else {
|
||||
|
@ -1358,7 +1361,7 @@ impl Workspace {
|
|||
let rx = this.update(&mut cx, |this, cx| {
|
||||
this.show_portal_error(err.to_string(), cx);
|
||||
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);
|
||||
rx
|
||||
})?;
|
||||
|
@ -1419,7 +1422,7 @@ impl Workspace {
|
|||
let project_path = abs_path.and_then(|abs_path| {
|
||||
this.update(&mut cx, |this, 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()
|
||||
|
@ -1703,6 +1706,7 @@ impl Workspace {
|
|||
directories: true,
|
||||
multiple: true,
|
||||
},
|
||||
DirectoryLister::Local(self.app_state.fs.clone()),
|
||||
cx,
|
||||
);
|
||||
|
||||
|
@ -1857,9 +1861,10 @@ impl Workspace {
|
|||
}
|
||||
|
||||
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(
|
||||
&anyhow!("Folders cannot yet be added to remote projects"),
|
||||
&anyhow!("You cannot add folders to someone else's project"),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
|
@ -1870,6 +1875,7 @@ impl Workspace {
|
|||
directories: true,
|
||||
multiple: true,
|
||||
},
|
||||
DirectoryLister::Project(self.project.clone()),
|
||||
cx,
|
||||
);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
|
@ -1895,7 +1901,7 @@ impl Workspace {
|
|||
cx: &mut AppContext,
|
||||
) -> Task<Result<(Model<Worktree>, ProjectPath)>> {
|
||||
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 {
|
||||
let (worktree, path) = entry.await?;
|
||||
|
@ -3852,7 +3858,7 @@ impl Workspace {
|
|||
let dev_server_project = SerializedDevServerProject {
|
||||
id: dev_server_project_id,
|
||||
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))
|
||||
})
|
||||
|
@ -3978,6 +3984,9 @@ impl Workspace {
|
|||
.on_action(cx.listener(Self::send_keystrokes))
|
||||
.on_action(cx.listener(Self::add_folder_to_project))
|
||||
.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| {
|
||||
let pane = workspace.active_pane().clone();
|
||||
workspace.unfollow_in_pane(&pane, cx);
|
||||
|
@ -4034,9 +4043,6 @@ impl Workspace {
|
|||
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(
|
||||
cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
|
||||
workspace.reopen_closed_item(cx).detach();
|
||||
|
@ -4083,13 +4089,7 @@ impl Workspace {
|
|||
self
|
||||
}
|
||||
|
||||
fn add_workspace_actions_listeners(&self, 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));
|
||||
fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
|
||||
for action in self.workspace_actions.iter() {
|
||||
div = (action)(div, cx)
|
||||
}
|
||||
|
@ -5506,7 +5506,7 @@ mod tests {
|
|||
// Add a project folder
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("root2", true, cx)
|
||||
project.find_or_create_worktree("root2", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -942,7 +942,7 @@ fn open_settings_file(
|
|||
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
|
||||
// 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);
|
||||
(worktree_creation_task, settings_open_task)
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
# 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.
|
||||
|
||||
> **Note:** Remoting is still "alpha". We have several changes we would like to make before it is fully released.
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
|
||||
> **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. Open the remote projects dialogue with `cmd-shift-p remote`.
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
@ -86,8 +84,8 @@ If you'd like to install language-server extensions, you can add them to the lis
|
|||
## Known Limitations
|
||||
|
||||
- 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 open files from the remote Terminal by typing the `zed` command.
|
||||
|
||||
## Feedback
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue