Restructure remote client crate, consolidate SSH logic (#36967)

This is a pure refactor that consolidates all SSH remoting logic such
that it should be straightforward to add another transport to the
remoting system.

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
This commit is contained in:
Max Brunsfeld
2025-08-27 00:15:39 +00:00
committed by GitHub
co-authored by Mikayla Maki
parent d713390366
commit 1eae76e856
39 changed files with 3330 additions and 3411 deletions
+1 -1
View File
@@ -334,7 +334,7 @@ impl<T: 'static> PromptEditor<T> {
EditorEvent::Edited { .. } => {
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
let is_via_ssh = workspace.project().read(cx).is_via_ssh();
let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
workspace
.client()
+1 -1
View File
@@ -1161,7 +1161,7 @@ impl Room {
let request = self.client.request(proto::ShareProject {
room_id: self.id(),
worktrees: project.read(cx).worktree_metadata_protos(cx),
is_ssh_project: project.read(cx).is_via_ssh(),
is_ssh_project: project.read(cx).is_via_remote_server(),
});
cx.spawn(async move |this, cx| {
@@ -26,7 +26,7 @@ use project::{
debugger::session::ThreadId,
lsp_store::{FormatTrigger, LspFormatTarget},
};
use remote::SshRemoteClient;
use remote::RemoteClient;
use remote_server::{HeadlessAppState, HeadlessProject};
use rpc::proto;
use serde_json::json;
@@ -59,7 +59,7 @@ async fn test_sharing_an_ssh_remote_project(
.await;
// Set up project on remote FS
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
@@ -101,7 +101,7 @@ async fn test_sharing_an_ssh_remote_project(
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
.await;
@@ -235,7 +235,7 @@ async fn test_ssh_collaboration_git_branches(
.await;
// Set up project on remote FS
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree("/project", serde_json::json!({ ".git":{} }))
@@ -268,7 +268,7 @@ async fn test_ssh_collaboration_git_branches(
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let (project_a, _) = client_a
.build_ssh_project("/project", client_ssh, cx_a)
.await;
@@ -420,7 +420,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
let buffer_text = "let one = \"two\"";
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
@@ -473,7 +473,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project(path!("/project"), client_ssh, cx_a)
.await;
@@ -602,7 +602,7 @@ async fn test_remote_server_debugger(
release_channel::init(SemanticVersion::default(), cx);
dap_adapters::init(cx);
});
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
@@ -633,7 +633,7 @@ async fn test_remote_server_debugger(
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let mut server = TestServer::start(server_cx.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
cx_a.update(|cx| {
@@ -711,7 +711,7 @@ async fn test_slow_adapter_startup_retries(
release_channel::init(SemanticVersion::default(), cx);
dap_adapters::init(cx);
});
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
@@ -742,7 +742,7 @@ async fn test_slow_adapter_startup_retries(
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let mut server = TestServer::start(server_cx.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
cx_a.update(|cx| {
+3 -3
View File
@@ -26,7 +26,7 @@ use node_runtime::NodeRuntime;
use notifications::NotificationStore;
use parking_lot::Mutex;
use project::{Project, WorktreeId};
use remote::SshRemoteClient;
use remote::RemoteClient;
use rpc::{
RECEIVE_TIMEOUT,
proto::{self, ChannelRole},
@@ -765,11 +765,11 @@ impl TestClient {
pub async fn build_ssh_project(
&self,
root_path: impl AsRef<Path>,
ssh: Entity<SshRemoteClient>,
ssh: Entity<RemoteClient>,
cx: &mut TestAppContext,
) -> (Entity<Project>, WorktreeId) {
let project = cx.update(|cx| {
Project::ssh(
Project::remote(
ssh,
self.client().clone(),
self.app_state.node_runtime.clone(),
+5 -4
View File
@@ -916,10 +916,11 @@ impl RunningState {
let task_store = project.read(cx).task_store().downgrade();
let weak_project = project.downgrade();
let weak_workspace = workspace.downgrade();
let ssh_info = project
let remote_shell = project
.read(cx)
.ssh_client()
.and_then(|it| it.read(cx).ssh_info());
.remote_client()
.as_ref()
.and_then(|remote| remote.read(cx).shell());
cx.spawn_in(window, async move |this, cx| {
let DebugScenario {
@@ -1003,7 +1004,7 @@ impl RunningState {
None
};
let builder = ShellBuilder::new(ssh_info.as_ref().map(|info| &*info.shell), &task.resolved.shell);
let builder = ShellBuilder::new(remote_shell.as_deref(), &task.resolved.shell);
let command_label = builder.command_label(&task.resolved.command_label);
let (command, args) =
builder.build(task.resolved.command.clone(), &task.resolved.args);
+3 -3
View File
@@ -20074,7 +20074,7 @@ impl Editor {
let (telemetry, is_via_ssh) = {
let project = project.read(cx);
let telemetry = project.client().telemetry().clone();
let is_via_ssh = project.is_via_ssh();
let is_via_ssh = project.is_via_remote_server();
(telemetry, is_via_ssh)
};
refresh_linked_ranges(self, window, cx);
@@ -20642,7 +20642,7 @@ impl Editor {
copilot_enabled,
copilot_enabled_for_language,
edit_predictions_provider,
is_via_ssh = project.is_via_ssh(),
is_via_ssh = project.is_via_remote_server(),
);
} else {
telemetry::event!(
@@ -20652,7 +20652,7 @@ impl Editor {
copilot_enabled,
copilot_enabled_for_language,
edit_predictions_provider,
is_via_ssh = project.is_via_ssh(),
is_via_ssh = project.is_via_remote_server(),
);
};
}
+9 -9
View File
@@ -43,7 +43,7 @@ use language::{
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
use release_channel::ReleaseChannel;
use remote::SshRemoteClient;
use remote::RemoteClient;
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -117,7 +117,7 @@ pub struct ExtensionStore {
pub wasm_host: Arc<WasmHost>,
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
pub tasks: Vec<Task<()>>,
pub ssh_clients: HashMap<String, WeakEntity<SshRemoteClient>>,
pub remote_clients: HashMap<String, WeakEntity<RemoteClient>>,
pub ssh_registered_tx: UnboundedSender<()>,
}
@@ -270,7 +270,7 @@ impl ExtensionStore {
reload_tx,
tasks: Vec::new(),
ssh_clients: HashMap::default(),
remote_clients: HashMap::default(),
ssh_registered_tx: connection_registered_tx,
};
@@ -1693,7 +1693,7 @@ impl ExtensionStore {
async fn sync_extensions_over_ssh(
this: &WeakEntity<Self>,
client: WeakEntity<SshRemoteClient>,
client: WeakEntity<RemoteClient>,
cx: &mut AsyncApp,
) -> Result<()> {
let extensions = this.update(cx, |this, _cx| {
@@ -1765,8 +1765,8 @@ impl ExtensionStore {
pub async fn update_ssh_clients(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
let clients = this.update(cx, |this, _cx| {
this.ssh_clients.retain(|_k, v| v.upgrade().is_some());
this.ssh_clients.values().cloned().collect::<Vec<_>>()
this.remote_clients.retain(|_k, v| v.upgrade().is_some());
this.remote_clients.values().cloned().collect::<Vec<_>>()
})?;
for client in clients {
@@ -1778,17 +1778,17 @@ impl ExtensionStore {
anyhow::Ok(())
}
pub fn register_ssh_client(&mut self, client: Entity<SshRemoteClient>, cx: &mut Context<Self>) {
pub fn register_remote_client(&mut self, client: Entity<RemoteClient>, cx: &mut Context<Self>) {
let connection_options = client.read(cx).connection_options();
let ssh_url = connection_options.ssh_url();
if let Some(existing_client) = self.ssh_clients.get(&ssh_url)
if let Some(existing_client) = self.remote_clients.get(&ssh_url)
&& existing_client.upgrade().is_some()
{
return;
}
self.ssh_clients.insert(ssh_url, client.downgrade());
self.remote_clients.insert(ssh_url, client.downgrade());
self.ssh_registered_tx.unbounded_send(()).ok();
}
}
+1 -1
View File
@@ -1381,7 +1381,7 @@ impl PickerDelegate for FileFinderDelegate {
project
.worktree_for_id(history_item.project.worktree_id, cx)
.is_some()
|| ((project.is_local() || project.is_via_ssh())
|| ((project.is_local() || project.is_via_remote_server())
&& history_item.absolute.is_some())
}),
self.currently_opened_path.as_ref(),
+3 -3
View File
@@ -222,7 +222,7 @@ pub fn init(cx: &mut App) {
cx.observe_new(move |workspace: &mut Workspace, _, cx| {
let project = workspace.project();
if project.read(cx).is_local() || project.read(cx).is_via_ssh() {
if project.read(cx).is_local() || project.read(cx).is_via_remote_server() {
log_store.update(cx, |store, cx| {
store.add_project(project, cx);
});
@@ -231,7 +231,7 @@ pub fn init(cx: &mut App) {
let log_store = log_store.clone();
workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| {
let project = workspace.project().read(cx);
if project.is_local() || project.is_via_ssh() {
if project.is_local() || project.is_via_remote_server() {
let project = workspace.project().clone();
let log_store = log_store.clone();
get_or_create_tool(
@@ -321,7 +321,7 @@ impl LogStore {
.retain(|_, state| state.kind.project() != Some(&weak_project));
}),
cx.subscribe(project, |this, project, event, cx| {
let server_kind = if project.read(cx).is_via_ssh() {
let server_kind = if project.read(cx).is_via_remote_server() {
LanguageServerKind::Remote {
project: project.downgrade(),
}
+3 -3
View File
@@ -5102,9 +5102,9 @@ impl EventEmitter<PanelEvent> for OutlinePanel {}
impl Render for OutlinePanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (is_local, is_via_ssh) = self
.project
.read_with(cx, |project, _| (project.is_local(), project.is_via_ssh()));
let (is_local, is_via_ssh) = self.project.read_with(cx, |project, _| {
(project.is_local(), project.is_via_remote_server())
});
let query = self.query(cx);
let pinned = self.pinned;
let settings = OutlinePanelSettings::get_global(cx);
+45 -61
View File
@@ -5,11 +5,8 @@ use super::{
session::{self, Session, SessionStateEvent},
};
use crate::{
InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState,
debugger::session::SessionQuirks,
project_settings::ProjectSettings,
terminals::{SshCommand, wrap_for_ssh},
worktree_store::WorktreeStore,
InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, debugger::session::SessionQuirks,
project_settings::ProjectSettings, worktree_store::WorktreeStore,
};
use anyhow::{Context as _, Result, anyhow};
use async_trait::async_trait;
@@ -34,7 +31,7 @@ use http_client::HttpClient;
use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind};
use node_runtime::NodeRuntime;
use remote::{SshInfo, SshRemoteClient, ssh_session::SshArgs};
use remote::RemoteClient;
use rpc::{
AnyProtoClient, TypedEnvelope,
proto::{self},
@@ -68,7 +65,7 @@ pub enum DapStoreEvent {
enum DapStoreMode {
Local(LocalDapStore),
Ssh(SshDapStore),
Remote(RemoteDapStore),
Collab,
}
@@ -80,8 +77,8 @@ pub struct LocalDapStore {
toolchain_store: Arc<dyn LanguageToolchainStore>,
}
pub struct SshDapStore {
ssh_client: Entity<SshRemoteClient>,
pub struct RemoteDapStore {
remote_client: Entity<RemoteClient>,
upstream_client: AnyProtoClient,
upstream_project_id: u64,
}
@@ -147,16 +144,16 @@ impl DapStore {
Self::new(mode, breakpoint_store, worktree_store, cx)
}
pub fn new_ssh(
pub fn new_remote(
project_id: u64,
ssh_client: Entity<SshRemoteClient>,
remote_client: Entity<RemoteClient>,
breakpoint_store: Entity<BreakpointStore>,
worktree_store: Entity<WorktreeStore>,
cx: &mut Context<Self>,
) -> Self {
let mode = DapStoreMode::Ssh(SshDapStore {
upstream_client: ssh_client.read(cx).proto_client(),
ssh_client,
let mode = DapStoreMode::Remote(RemoteDapStore {
upstream_client: remote_client.read(cx).proto_client(),
remote_client,
upstream_project_id: project_id,
});
@@ -242,64 +239,51 @@ impl DapStore {
Ok(binary)
})
}
DapStoreMode::Ssh(ssh) => {
let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary {
session_id: session_id.to_proto(),
project_id: ssh.upstream_project_id,
worktree_id: worktree.read(cx).id().to_proto(),
definition: Some(definition.to_proto()),
});
let ssh_client = ssh.ssh_client.clone();
DapStoreMode::Remote(remote) => {
let request = remote
.upstream_client
.request(proto::GetDebugAdapterBinary {
session_id: session_id.to_proto(),
project_id: remote.upstream_project_id,
worktree_id: worktree.read(cx).id().to_proto(),
definition: Some(definition.to_proto()),
});
let remote = remote.remote_client.clone();
cx.spawn(async move |_, cx| {
let response = request.await?;
let binary = DebugAdapterBinary::from_proto(response)?;
let (mut ssh_command, envs, path_style, ssh_shell) =
ssh_client.read_with(cx, |ssh, _| {
let SshInfo {
args: SshArgs { arguments, envs },
path_style,
shell,
} = ssh.ssh_info().context("SSH arguments not found")?;
anyhow::Ok((
SshCommand { arguments },
envs.unwrap_or_default(),
path_style,
shell,
))
})??;
let mut connection = None;
let port_forwarding;
let connection;
if let Some(c) = binary.connection {
let local_bind_addr = Ipv4Addr::LOCALHOST;
let port =
dap::transport::TcpTransport::unused_port(local_bind_addr).await?;
ssh_command.add_port_forwarding(port, c.host.to_string(), c.port);
let host = Ipv4Addr::LOCALHOST;
let port = dap::transport::TcpTransport::unused_port(host).await?;
port_forwarding = Some((port, c.host.to_string(), c.port));
connection = Some(TcpArguments {
port,
host: local_bind_addr,
host,
timeout: c.timeout,
})
} else {
port_forwarding = None;
connection = None;
}
let (program, args) = wrap_for_ssh(
&ssh_shell,
&ssh_command,
binary
.command
.as_ref()
.map(|command| (command, &binary.arguments)),
binary.cwd.as_deref(),
binary.envs,
None,
path_style,
);
let command = remote.read_with(cx, |remote, _cx| {
remote.build_command(
binary.command,
&binary.arguments,
&binary.envs,
binary.cwd.map(|path| path.display().to_string()),
port_forwarding,
)
})??;
Ok(DebugAdapterBinary {
command: Some(program),
arguments: args,
envs,
command: Some(command.program),
arguments: command.args,
envs: command.env,
cwd: None,
connection,
request_args: binary.request_args,
@@ -365,9 +349,9 @@ impl DapStore {
)))
}
}
DapStoreMode::Ssh(ssh) => {
let request = ssh.upstream_client.request(proto::RunDebugLocators {
project_id: ssh.upstream_project_id,
DapStoreMode::Remote(remote) => {
let request = remote.upstream_client.request(proto::RunDebugLocators {
project_id: remote.upstream_project_id,
build_command: Some(build_command.to_proto()),
locator: locator_name.to_owned(),
});
+17 -51
View File
@@ -44,7 +44,7 @@ use parking_lot::Mutex;
use postage::stream::Stream as _;
use rpc::{
AnyProtoClient, TypedEnvelope,
proto::{self, FromProto, SSH_PROJECT_ID, ToProto, git_reset, split_repository_update},
proto::{self, FromProto, ToProto, git_reset, split_repository_update},
};
use serde::Deserialize;
use std::{
@@ -141,14 +141,10 @@ enum GitStoreState {
project_environment: Entity<ProjectEnvironment>,
fs: Arc<dyn Fs>,
},
Ssh {
upstream_client: AnyProtoClient,
upstream_project_id: ProjectId,
downstream: Option<(AnyProtoClient, ProjectId)>,
},
Remote {
upstream_client: AnyProtoClient,
upstream_project_id: ProjectId,
upstream_project_id: u64,
downstream: Option<(AnyProtoClient, ProjectId)>,
},
}
@@ -355,7 +351,7 @@ impl GitStore {
worktree_store: &Entity<WorktreeStore>,
buffer_store: Entity<BufferStore>,
upstream_client: AnyProtoClient,
project_id: ProjectId,
project_id: u64,
cx: &mut Context<Self>,
) -> Self {
Self::new(
@@ -364,23 +360,6 @@ impl GitStore {
GitStoreState::Remote {
upstream_client,
upstream_project_id: project_id,
},
cx,
)
}
pub fn ssh(
worktree_store: &Entity<WorktreeStore>,
buffer_store: Entity<BufferStore>,
upstream_client: AnyProtoClient,
cx: &mut Context<Self>,
) -> Self {
Self::new(
worktree_store.clone(),
buffer_store,
GitStoreState::Ssh {
upstream_client,
upstream_project_id: ProjectId(SSH_PROJECT_ID),
downstream: None,
},
cx,
@@ -451,7 +430,7 @@ impl GitStore {
pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
match &mut self.state {
GitStoreState::Ssh {
GitStoreState::Remote {
downstream: downstream_client,
..
} => {
@@ -527,9 +506,6 @@ impl GitStore {
}),
});
}
GitStoreState::Remote { .. } => {
debug_panic!("shared called on remote store");
}
}
}
@@ -541,15 +517,12 @@ impl GitStore {
} => {
downstream_client.take();
}
GitStoreState::Ssh {
GitStoreState::Remote {
downstream: downstream_client,
..
} => {
downstream_client.take();
}
GitStoreState::Remote { .. } => {
debug_panic!("unshared called on remote store");
}
}
self.shared_diffs.clear();
}
@@ -1047,21 +1020,17 @@ impl GitStore {
} => downstream_client
.as_ref()
.map(|state| (state.client.clone(), state.project_id)),
GitStoreState::Ssh {
GitStoreState::Remote {
downstream: downstream_client,
..
} => downstream_client.clone(),
GitStoreState::Remote { .. } => None,
}
}
fn upstream_client(&self) -> Option<AnyProtoClient> {
match &self.state {
GitStoreState::Local { .. } => None,
GitStoreState::Ssh {
upstream_client, ..
}
| GitStoreState::Remote {
GitStoreState::Remote {
upstream_client, ..
} => Some(upstream_client.clone()),
}
@@ -1432,12 +1401,7 @@ impl GitStore {
cx.background_executor()
.spawn(async move { fs.git_init(&path, fallback_branch_name) })
}
GitStoreState::Ssh {
upstream_client,
upstream_project_id: project_id,
..
}
| GitStoreState::Remote {
GitStoreState::Remote {
upstream_client,
upstream_project_id: project_id,
..
@@ -1447,7 +1411,7 @@ impl GitStore {
cx.background_executor().spawn(async move {
client
.request(proto::GitInit {
project_id: project_id.0,
project_id: project_id,
abs_path: path.to_string_lossy().to_string(),
fallback_branch_name,
})
@@ -1471,13 +1435,18 @@ impl GitStore {
cx.background_executor()
.spawn(async move { fs.git_clone(&repo, &path).await })
}
GitStoreState::Ssh {
GitStoreState::Remote {
upstream_client,
upstream_project_id,
..
} => {
if upstream_client.is_via_collab() {
return Task::ready(Err(anyhow!(
"Git Clone isn't supported for project guests"
)));
}
let request = upstream_client.request(proto::GitClone {
project_id: upstream_project_id.0,
project_id: *upstream_project_id,
abs_path: path.to_string_lossy().to_string(),
remote_repo: repo,
});
@@ -1491,9 +1460,6 @@ impl GitStore {
}
})
}
GitStoreState::Remote { .. } => {
Task::ready(Err(anyhow!("Git Clone isn't supported for remote users")))
}
}
}
+154 -155
View File
@@ -42,9 +42,7 @@ pub use manifest_tree::ManifestTree;
use anyhow::{Context as _, Result, anyhow};
use buffer_store::{BufferStore, BufferStoreEvent};
use client::{
Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore, proto,
};
use client::{Client, Collaborator, PendingEntitySubscription, TypedEnvelope, UserStore, proto};
use clock::ReplicaId;
use dap::client::DebugAdapterClient;
@@ -89,10 +87,10 @@ use node_runtime::NodeRuntime;
use parking_lot::Mutex;
pub use prettier_store::PrettierStore;
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
use remote::{SshConnectionOptions, SshRemoteClient};
use remote::{RemoteClient, SshConnectionOptions};
use rpc::{
AnyProtoClient, ErrorCode,
proto::{FromProto, LanguageServerPromptResponse, SSH_PROJECT_ID, ToProto},
proto::{FromProto, LanguageServerPromptResponse, REMOTE_SERVER_PROJECT_ID, ToProto},
};
use search::{SearchInputKind, SearchQuery, SearchResult};
use search_history::SearchHistory;
@@ -177,12 +175,12 @@ pub struct Project {
dap_store: Entity<DapStore>,
breakpoint_store: Entity<BreakpointStore>,
client: Arc<client::Client>,
collab_client: Arc<client::Client>,
join_project_response_message_id: u32,
task_store: Entity<TaskStore>,
user_store: Entity<UserStore>,
fs: Arc<dyn Fs>,
ssh_client: Option<Entity<SshRemoteClient>>,
remote_client: Option<Entity<RemoteClient>>,
client_state: ProjectClientState,
git_store: Entity<GitStore>,
collaborators: HashMap<proto::PeerId, Collaborator>,
@@ -1154,12 +1152,12 @@ impl Project {
active_entry: None,
snippets,
languages,
client,
collab_client: client,
task_store,
user_store,
settings_observer,
fs,
ssh_client: None,
remote_client: None,
breakpoint_store,
dap_store,
@@ -1183,8 +1181,8 @@ impl Project {
})
}
pub fn ssh(
ssh: Entity<SshRemoteClient>,
pub fn remote(
remote: Entity<RemoteClient>,
client: Arc<Client>,
node: NodeRuntime,
user_store: Entity<UserStore>,
@@ -1200,10 +1198,15 @@ impl Project {
let snippets =
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
let (ssh_proto, path_style) =
ssh.read_with(cx, |ssh, _| (ssh.proto_client(), ssh.path_style()));
let (remote_proto, path_style) =
remote.read_with(cx, |remote, _| (remote.proto_client(), remote.path_style()));
let worktree_store = cx.new(|_| {
WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID, path_style)
WorktreeStore::remote(
false,
remote_proto.clone(),
REMOTE_SERVER_PROJECT_ID,
path_style,
)
});
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach();
@@ -1215,31 +1218,32 @@ impl Project {
let buffer_store = cx.new(|cx| {
BufferStore::remote(
worktree_store.clone(),
ssh.read(cx).proto_client(),
SSH_PROJECT_ID,
remote.read(cx).proto_client(),
REMOTE_SERVER_PROJECT_ID,
cx,
)
});
let image_store = cx.new(|cx| {
ImageStore::remote(
worktree_store.clone(),
ssh.read(cx).proto_client(),
SSH_PROJECT_ID,
remote.read(cx).proto_client(),
REMOTE_SERVER_PROJECT_ID,
cx,
)
});
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
.detach();
let toolchain_store = cx
.new(|cx| ToolchainStore::remote(SSH_PROJECT_ID, ssh.read(cx).proto_client(), cx));
let toolchain_store = cx.new(|cx| {
ToolchainStore::remote(REMOTE_SERVER_PROJECT_ID, remote.read(cx).proto_client(), cx)
});
let task_store = cx.new(|cx| {
TaskStore::remote(
fs.clone(),
buffer_store.downgrade(),
worktree_store.clone(),
toolchain_store.read(cx).as_language_toolchain_store(),
ssh.read(cx).proto_client(),
SSH_PROJECT_ID,
remote.read(cx).proto_client(),
REMOTE_SERVER_PROJECT_ID,
cx,
)
});
@@ -1262,8 +1266,8 @@ impl Project {
buffer_store.clone(),
worktree_store.clone(),
languages.clone(),
ssh_proto.clone(),
SSH_PROJECT_ID,
remote_proto.clone(),
REMOTE_SERVER_PROJECT_ID,
fs.clone(),
cx,
)
@@ -1271,12 +1275,12 @@ impl Project {
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
let breakpoint_store =
cx.new(|_| BreakpointStore::remote(SSH_PROJECT_ID, ssh_proto.clone()));
cx.new(|_| BreakpointStore::remote(REMOTE_SERVER_PROJECT_ID, remote_proto.clone()));
let dap_store = cx.new(|cx| {
DapStore::new_ssh(
SSH_PROJECT_ID,
ssh.clone(),
DapStore::new_remote(
REMOTE_SERVER_PROJECT_ID,
remote.clone(),
breakpoint_store.clone(),
worktree_store.clone(),
cx,
@@ -1284,10 +1288,16 @@ impl Project {
});
let git_store = cx.new(|cx| {
GitStore::ssh(&worktree_store, buffer_store.clone(), ssh_proto.clone(), cx)
GitStore::remote(
&worktree_store,
buffer_store.clone(),
remote_proto.clone(),
REMOTE_SERVER_PROJECT_ID,
cx,
)
});
cx.subscribe(&ssh, Self::on_ssh_event).detach();
cx.subscribe(&remote, Self::on_remote_client_event).detach();
let this = Self {
buffer_ordered_messages_tx: tx,
@@ -1306,11 +1316,13 @@ impl Project {
_subscriptions: vec![
cx.on_release(Self::release),
cx.on_app_quit(|this, cx| {
let shutdown = this.ssh_client.take().and_then(|client| {
client.read(cx).shutdown_processes(
Some(proto::ShutdownRemoteServer {}),
cx.background_executor().clone(),
)
let shutdown = this.remote_client.take().and_then(|client| {
client.update(cx, |client, cx| {
client.shutdown_processes(
Some(proto::ShutdownRemoteServer {}),
cx.background_executor().clone(),
)
})
});
cx.background_executor().spawn(async move {
@@ -1323,12 +1335,12 @@ impl Project {
active_entry: None,
snippets,
languages,
client,
collab_client: client,
task_store,
user_store,
settings_observer,
fs,
ssh_client: Some(ssh.clone()),
remote_client: Some(remote.clone()),
buffers_needing_diff: Default::default(),
git_diff_debouncer: DebouncedDelay::new(),
terminals: Terminals {
@@ -1346,52 +1358,34 @@ impl Project {
agent_location: None,
};
// ssh -> local machine handlers
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &cx.entity());
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store);
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store);
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store);
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.dap_store);
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer);
ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.git_store);
// remote server -> local machine handlers
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &cx.entity());
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.buffer_store);
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.worktree_store);
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.lsp_store);
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.dap_store);
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.settings_observer);
remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.git_store);
ssh_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer);
ssh_proto.add_entity_message_handler(Self::handle_update_worktree);
ssh_proto.add_entity_message_handler(Self::handle_update_project);
ssh_proto.add_entity_message_handler(Self::handle_toast);
ssh_proto.add_entity_request_handler(Self::handle_language_server_prompt_request);
ssh_proto.add_entity_message_handler(Self::handle_hide_toast);
ssh_proto.add_entity_request_handler(Self::handle_update_buffer_from_ssh);
BufferStore::init(&ssh_proto);
LspStore::init(&ssh_proto);
SettingsObserver::init(&ssh_proto);
TaskStore::init(Some(&ssh_proto));
ToolchainStore::init(&ssh_proto);
DapStore::init(&ssh_proto, cx);
GitStore::init(&ssh_proto);
remote_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer);
remote_proto.add_entity_message_handler(Self::handle_update_worktree);
remote_proto.add_entity_message_handler(Self::handle_update_project);
remote_proto.add_entity_message_handler(Self::handle_toast);
remote_proto.add_entity_request_handler(Self::handle_language_server_prompt_request);
remote_proto.add_entity_message_handler(Self::handle_hide_toast);
remote_proto.add_entity_request_handler(Self::handle_update_buffer_from_remote_server);
BufferStore::init(&remote_proto);
LspStore::init(&remote_proto);
SettingsObserver::init(&remote_proto);
TaskStore::init(Some(&remote_proto));
ToolchainStore::init(&remote_proto);
DapStore::init(&remote_proto, cx);
GitStore::init(&remote_proto);
this
})
}
pub async fn remote(
remote_id: u64,
client: Arc<Client>,
user_store: Entity<UserStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: AsyncApp,
) -> Result<Entity<Self>> {
let project =
Self::in_room(remote_id, client, user_store, languages, fs, cx.clone()).await?;
cx.update(|cx| {
connection_manager::Manager::global(cx).update(cx, |manager, cx| {
manager.maintain_project_connection(&project, cx)
})
})?;
Ok(project)
}
pub async fn in_room(
remote_id: u64,
client: Arc<Client>,
@@ -1523,7 +1517,7 @@ impl Project {
&worktree_store,
buffer_store.clone(),
client.clone().into(),
ProjectId(remote_id),
remote_id,
cx,
)
})?;
@@ -1574,11 +1568,11 @@ impl Project {
task_store,
snippets,
fs,
ssh_client: None,
remote_client: None,
settings_observer: settings_observer.clone(),
client_subscriptions: Default::default(),
_subscriptions: vec![cx.on_release(Self::release)],
client: client.clone(),
collab_client: client.clone(),
client_state: ProjectClientState::Remote {
sharing_has_stopped: false,
capability: Capability::ReadWrite,
@@ -1661,11 +1655,13 @@ impl Project {
}
fn release(&mut self, cx: &mut App) {
if let Some(client) = self.ssh_client.take() {
let shutdown = client.read(cx).shutdown_processes(
Some(proto::ShutdownRemoteServer {}),
cx.background_executor().clone(),
);
if let Some(client) = self.remote_client.take() {
let shutdown = client.update(cx, |client, cx| {
client.shutdown_processes(
Some(proto::ShutdownRemoteServer {}),
cx.background_executor().clone(),
)
});
cx.background_spawn(async move {
if let Some(shutdown) = shutdown {
@@ -1681,7 +1677,7 @@ impl Project {
let _ = self.unshare_internal(cx);
}
ProjectClientState::Remote { remote_id, .. } => {
let _ = self.client.send(proto::LeaveProject {
let _ = self.collab_client.send(proto::LeaveProject {
project_id: *remote_id,
});
self.disconnected_from_host_internal(cx);
@@ -1808,11 +1804,11 @@ impl Project {
}
pub fn client(&self) -> Arc<Client> {
self.client.clone()
self.collab_client.clone()
}
pub fn ssh_client(&self) -> Option<Entity<SshRemoteClient>> {
self.ssh_client.clone()
pub fn remote_client(&self) -> Option<Entity<RemoteClient>> {
self.remote_client.clone()
}
pub fn user_store(&self) -> Entity<UserStore> {
@@ -1893,30 +1889,30 @@ impl Project {
if self.is_local() {
return true;
}
if self.is_via_ssh() {
if self.is_via_remote_server() {
return true;
}
false
}
pub fn ssh_connection_state(&self, cx: &App) -> Option<remote::ConnectionState> {
self.ssh_client
pub fn remote_connection_state(&self, cx: &App) -> Option<remote::ConnectionState> {
self.remote_client
.as_ref()
.map(|ssh| ssh.read(cx).connection_state())
.map(|remote| remote.read(cx).connection_state())
}
pub fn ssh_connection_options(&self, cx: &App) -> Option<SshConnectionOptions> {
self.ssh_client
pub fn remote_connection_options(&self, cx: &App) -> Option<SshConnectionOptions> {
self.remote_client
.as_ref()
.map(|ssh| ssh.read(cx).connection_options())
.map(|remote| remote.read(cx).connection_options())
}
pub fn replica_id(&self) -> ReplicaId {
match self.client_state {
ProjectClientState::Remote { replica_id, .. } => replica_id,
_ => {
if self.ssh_client.is_some() {
if self.remote_client.is_some() {
1
} else {
0
@@ -2220,55 +2216,55 @@ impl Project {
);
self.client_subscriptions.extend([
self.client
self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&cx.entity(), &cx.to_async()),
self.client
self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&self.worktree_store, &cx.to_async()),
self.client
self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&self.buffer_store, &cx.to_async()),
self.client
self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&self.lsp_store, &cx.to_async()),
self.client
self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&self.settings_observer, &cx.to_async()),
self.client
self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&self.dap_store, &cx.to_async()),
self.client
self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&self.breakpoint_store, &cx.to_async()),
self.client
self.collab_client
.subscribe_to_entity(project_id)?
.set_entity(&self.git_store, &cx.to_async()),
]);
self.buffer_store.update(cx, |buffer_store, cx| {
buffer_store.shared(project_id, self.client.clone().into(), cx)
buffer_store.shared(project_id, self.collab_client.clone().into(), cx)
});
self.worktree_store.update(cx, |worktree_store, cx| {
worktree_store.shared(project_id, self.client.clone().into(), cx);
worktree_store.shared(project_id, self.collab_client.clone().into(), cx);
});
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.shared(project_id, self.client.clone().into(), cx)
lsp_store.shared(project_id, self.collab_client.clone().into(), cx)
});
self.breakpoint_store.update(cx, |breakpoint_store, _| {
breakpoint_store.shared(project_id, self.client.clone().into())
breakpoint_store.shared(project_id, self.collab_client.clone().into())
});
self.dap_store.update(cx, |dap_store, cx| {
dap_store.shared(project_id, self.client.clone().into(), cx);
dap_store.shared(project_id, self.collab_client.clone().into(), cx);
});
self.task_store.update(cx, |task_store, cx| {
task_store.shared(project_id, self.client.clone().into(), cx);
task_store.shared(project_id, self.collab_client.clone().into(), cx);
});
self.settings_observer.update(cx, |settings_observer, cx| {
settings_observer.shared(project_id, self.client.clone().into(), cx)
settings_observer.shared(project_id, self.collab_client.clone().into(), cx)
});
self.git_store.update(cx, |git_store, cx| {
git_store.shared(project_id, self.client.clone().into(), cx)
git_store.shared(project_id, self.collab_client.clone().into(), cx)
});
self.client_state = ProjectClientState::Shared {
@@ -2293,7 +2289,7 @@ impl Project {
});
if let Some(remote_id) = self.remote_id() {
self.git_store.update(cx, |git_store, cx| {
git_store.shared(remote_id, self.client.clone().into(), cx)
git_store.shared(remote_id, self.collab_client.clone().into(), cx)
});
}
cx.emit(Event::Reshared);
@@ -2370,7 +2366,7 @@ impl Project {
git_store.unshared(cx);
});
self.client
self.collab_client
.send(proto::UnshareProject {
project_id: remote_id,
})
@@ -2437,15 +2433,17 @@ impl Project {
sharing_has_stopped,
..
} => *sharing_has_stopped,
ProjectClientState::Local if self.is_via_ssh() => self.ssh_is_disconnected(cx),
ProjectClientState::Local if self.is_via_remote_server() => {
self.remote_client_is_disconnected(cx)
}
_ => false,
}
}
fn ssh_is_disconnected(&self, cx: &App) -> bool {
self.ssh_client
fn remote_client_is_disconnected(&self, cx: &App) -> bool {
self.remote_client
.as_ref()
.map(|ssh| ssh.read(cx).is_disconnected())
.map(|remote| remote.read(cx).is_disconnected())
.unwrap_or(false)
}
@@ -2463,16 +2461,16 @@ impl Project {
pub fn is_local(&self) -> bool {
match &self.client_state {
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
self.ssh_client.is_none()
self.remote_client.is_none()
}
ProjectClientState::Remote { .. } => false,
}
}
pub fn is_via_ssh(&self) -> bool {
pub fn is_via_remote_server(&self) -> bool {
match &self.client_state {
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
self.ssh_client.is_some()
self.remote_client.is_some()
}
ProjectClientState::Remote { .. } => false,
}
@@ -2496,7 +2494,7 @@ impl Project {
language: Option<Arc<Language>>,
cx: &mut Context<Self>,
) -> Entity<Buffer> {
if self.is_via_collab() || self.is_via_ssh() {
if self.is_via_collab() || self.is_via_remote_server() {
panic!("called create_local_buffer on a remote project")
}
self.buffer_store.update(cx, |buffer_store, cx| {
@@ -2620,10 +2618,10 @@ impl Project {
) -> Task<Result<Entity<Buffer>>> {
if let Some(buffer) = self.buffer_for_id(id, cx) {
Task::ready(Ok(buffer))
} else if self.is_local() || self.is_via_ssh() {
} else if self.is_local() || self.is_via_remote_server() {
Task::ready(Err(anyhow!("buffer {id} does not exist")))
} else if let Some(project_id) = self.remote_id() {
let request = self.client.request(proto::OpenBufferById {
let request = self.collab_client.request(proto::OpenBufferById {
project_id,
id: id.into(),
});
@@ -2741,7 +2739,7 @@ impl Project {
for (buffer_id, operations) in operations_by_buffer_id.drain() {
let request = this.read_with(cx, |this, _| {
let project_id = this.remote_id()?;
Some(this.client.request(proto::UpdateBuffer {
Some(this.collab_client.request(proto::UpdateBuffer {
buffer_id: buffer_id.into(),
project_id,
operations,
@@ -2808,7 +2806,7 @@ impl Project {
project.read_with(cx, |project, _| {
if let Some(project_id) = project.remote_id() {
project
.client
.collab_client
.send(proto::UpdateLanguageServer {
project_id,
server_name: name.map(|name| String::from(name.0)),
@@ -2846,8 +2844,8 @@ impl Project {
self.register_buffer(buffer, cx).log_err();
}
BufferStoreEvent::BufferDropped(buffer_id) => {
if let Some(ref ssh_client) = self.ssh_client {
ssh_client
if let Some(ref remote_client) = self.remote_client {
remote_client
.read(cx)
.proto_client()
.send(proto::CloseBuffer {
@@ -2995,16 +2993,14 @@ impl Project {
}
}
fn on_ssh_event(
fn on_remote_client_event(
&mut self,
_: Entity<SshRemoteClient>,
event: &remote::SshRemoteEvent,
_: Entity<RemoteClient>,
event: &remote::RemoteClientEvent,
cx: &mut Context<Self>,
) {
match event {
remote::SshRemoteEvent::Disconnected => {
// if self.is_via_ssh() {
// self.collaborators.clear();
remote::RemoteClientEvent::Disconnected => {
self.worktree_store.update(cx, |store, cx| {
store.disconnected_from_host(cx);
});
@@ -3110,8 +3106,9 @@ impl Project {
}
fn on_worktree_released(&mut self, id_to_remove: WorktreeId, cx: &mut Context<Self>) {
if let Some(ssh) = &self.ssh_client {
ssh.read(cx)
if let Some(remote) = &self.remote_client {
remote
.read(cx)
.proto_client()
.send(proto::RemoveWorktree {
worktree_id: id_to_remove.to_proto(),
@@ -3144,8 +3141,9 @@ impl Project {
} => {
let operation = language::proto::serialize_operation(operation);
if let Some(ssh) = &self.ssh_client {
ssh.read(cx)
if let Some(remote) = &self.remote_client {
remote
.read(cx)
.proto_client()
.send(proto::UpdateBuffer {
project_id: 0,
@@ -3552,16 +3550,16 @@ impl Project {
pub fn open_server_settings(&mut self, cx: &mut Context<Self>) -> Task<Result<Entity<Buffer>>> {
let guard = self.retain_remotely_created_models(cx);
let Some(ssh_client) = self.ssh_client.as_ref() else {
let Some(remote) = self.remote_client.as_ref() else {
return Task::ready(Err(anyhow!("not an ssh project")));
};
let proto_client = ssh_client.read(cx).proto_client();
let proto_client = remote.read(cx).proto_client();
cx.spawn(async move |project, cx| {
let buffer = proto_client
.request(proto::OpenServerSettings {
project_id: SSH_PROJECT_ID,
project_id: REMOTE_SERVER_PROJECT_ID,
})
.await?;
@@ -3948,10 +3946,11 @@ impl Project {
) -> Receiver<Entity<Buffer>> {
let (tx, rx) = smol::channel::unbounded();
let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.ssh_client {
let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.remote_client
{
(ssh_client.read(cx).proto_client(), 0)
} else if let Some(remote_id) = self.remote_id() {
(self.client.clone().into(), remote_id)
(self.collab_client.clone().into(), remote_id)
} else {
return rx;
};
@@ -4095,14 +4094,14 @@ impl Project {
is_dir: metadata.is_dir,
})
})
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
} else if let Some(ssh_client) = self.remote_client.as_ref() {
let path_style = ssh_client.read(cx).path_style();
let request_path = RemotePathBuf::from_str(path, path_style);
let request = ssh_client
.read(cx)
.proto_client()
.request(proto::GetPathMetadata {
project_id: SSH_PROJECT_ID,
project_id: REMOTE_SERVER_PROJECT_ID,
path: request_path.to_proto(),
});
cx.background_spawn(async move {
@@ -4202,10 +4201,10 @@ impl Project {
) -> Task<Result<Vec<DirectoryItem>>> {
if self.is_local() {
DirectoryLister::Local(cx.entity(), self.fs.clone()).list_directory(query, cx)
} else if let Some(session) = self.ssh_client.as_ref() {
} else if let Some(session) = self.remote_client.as_ref() {
let path_buf = PathBuf::from(query);
let request = proto::ListRemoteDirectory {
dev_server_id: SSH_PROJECT_ID,
dev_server_id: REMOTE_SERVER_PROJECT_ID,
path: path_buf.to_proto(),
config: Some(proto::ListRemoteDirectoryConfig { is_dir: true }),
};
@@ -4420,7 +4419,7 @@ impl Project {
mut cx: AsyncApp,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
if this.is_local() || this.is_via_ssh() {
if this.is_local() || this.is_via_remote_server() {
this.unshare(cx)?;
} else {
this.disconnected_from_host(cx);
@@ -4629,7 +4628,7 @@ impl Project {
})?
}
async fn handle_update_buffer_from_ssh(
async fn handle_update_buffer_from_remote_server(
this: Entity<Self>,
envelope: TypedEnvelope<proto::UpdateBuffer>,
cx: AsyncApp,
@@ -4638,7 +4637,7 @@ impl Project {
if let Some(remote_id) = this.remote_id() {
let mut payload = envelope.payload.clone();
payload.project_id = remote_id;
cx.background_spawn(this.client.request(payload))
cx.background_spawn(this.collab_client.request(payload))
.detach_and_log_err(cx);
}
this.buffer_store.clone()
@@ -4652,9 +4651,9 @@ impl Project {
cx: AsyncApp,
) -> Result<proto::Ack> {
let buffer_store = this.read_with(&cx, |this, cx| {
if let Some(ssh) = &this.ssh_client {
if let Some(ssh) = &this.remote_client {
let mut payload = envelope.payload.clone();
payload.project_id = SSH_PROJECT_ID;
payload.project_id = REMOTE_SERVER_PROJECT_ID;
cx.background_spawn(ssh.read(cx).proto_client().request(payload))
.detach_and_log_err(cx);
}
@@ -4704,7 +4703,7 @@ impl Project {
mut cx: AsyncApp,
) -> Result<proto::SynchronizeBuffersResponse> {
let response = this.update(&mut cx, |this, cx| {
let client = this.client.clone();
let client = this.collab_client.clone();
this.buffer_store.update(cx, |this, cx| {
this.handle_synchronize_buffers(envelope, cx, client)
})
@@ -4841,7 +4840,7 @@ impl Project {
}
};
let client = self.client.clone();
let client = self.collab_client.clone();
cx.spawn(async move |this, cx| {
let (buffers, incomplete_buffer_ids) = this.update(cx, |this, cx| {
this.buffer_store.read(cx).buffer_version_info(cx)
+109 -242
View File
@@ -2,13 +2,11 @@ use crate::{Project, ProjectPath};
use anyhow::{Context as _, Result};
use collections::HashMap;
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
use itertools::Itertools;
use language::LanguageName;
use remote::{SshInfo, ssh_session::SshArgs};
use remote::RemoteClient;
use settings::{Settings, SettingsLocation};
use smol::channel::bounded;
use std::{
borrow::Cow,
env::{self},
path::{Path, PathBuf},
sync::Arc,
@@ -18,10 +16,7 @@ use terminal::{
TaskState, TaskStatus, Terminal, TerminalBuilder,
terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings},
};
use util::{
ResultExt,
paths::{PathStyle, RemotePathBuf},
};
use util::{ResultExt, paths::RemotePathBuf};
/// The directory inside a Python virtual environment that contains executables
const PYTHON_VENV_BIN_DIR: &str = if cfg!(target_os = "windows") {
@@ -44,29 +39,6 @@ pub enum TerminalKind {
Task(SpawnInTerminal),
}
/// SshCommand describes how to connect to a remote server
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SshCommand {
pub arguments: Vec<String>,
}
impl SshCommand {
pub fn add_port_forwarding(&mut self, local_port: u16, host: String, remote_port: u16) {
self.arguments.push("-L".to_string());
self.arguments
.push(format!("{}:{}:{}", local_port, host, remote_port));
}
}
#[derive(Debug)]
pub struct SshDetails {
pub host: String,
pub ssh_command: SshCommand,
pub envs: Option<HashMap<String, String>>,
pub path_style: PathStyle,
pub shell: String,
}
impl Project {
pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
self.active_entry()
@@ -86,28 +58,6 @@ impl Project {
}
}
pub fn ssh_details(&self, cx: &App) -> Option<SshDetails> {
if let Some(ssh_client) = &self.ssh_client {
let ssh_client = ssh_client.read(cx);
if let Some(SshInfo {
args: SshArgs { arguments, envs },
path_style,
shell,
}) = ssh_client.ssh_info()
{
return Some(SshDetails {
host: ssh_client.connection_options().host,
ssh_command: SshCommand { arguments },
envs,
path_style,
shell,
});
}
}
None
}
pub fn create_terminal(
&mut self,
kind: TerminalKind,
@@ -168,14 +118,14 @@ impl Project {
TerminalSettings::get(settings_location, cx)
}
pub fn exec_in_shell(&self, command: String, cx: &App) -> std::process::Command {
pub fn exec_in_shell(&self, command: String, cx: &App) -> Result<std::process::Command> {
let path = self.first_project_directory(cx);
let ssh_details = self.ssh_details(cx);
let remote_client = self.remote_client.as_ref();
let settings = self.terminal_settings(&path, cx).clone();
let builder =
ShellBuilder::new(ssh_details.as_ref().map(|ssh| &*ssh.shell), &settings.shell)
.non_interactive();
let remote_shell = remote_client
.as_ref()
.and_then(|remote_client| remote_client.read(cx).shell());
let builder = ShellBuilder::new(remote_shell.as_deref(), &settings.shell).non_interactive();
let (command, args) = builder.build(Some(command), &Vec::new());
let mut env = self
@@ -185,29 +135,16 @@ impl Project {
.unwrap_or_default();
env.extend(settings.env);
match self.ssh_details(cx) {
Some(SshDetails {
ssh_command,
envs,
path_style,
shell,
..
}) => {
let (command, args) = wrap_for_ssh(
&shell,
&ssh_command,
Some((&command, &args)),
path.as_deref(),
env,
None,
path_style,
);
let mut command = std::process::Command::new(command);
command.args(args);
if let Some(envs) = envs {
command.envs(envs);
}
command
match remote_client {
Some(remote_client) => {
let command_template =
remote_client
.read(cx)
.build_command(Some(command), &args, &env, None, None)?;
let mut command = std::process::Command::new(command_template.program);
command.args(command_template.args);
command.envs(command_template.env);
Ok(command)
}
None => {
let mut command = std::process::Command::new(command);
@@ -216,7 +153,7 @@ impl Project {
if let Some(path) = path {
command.current_dir(path);
}
command
Ok(command)
}
}
}
@@ -227,13 +164,13 @@ impl Project {
python_venv_directory: Option<PathBuf>,
cx: &mut Context<Self>,
) -> Result<Entity<Terminal>> {
let this = &mut *self;
let ssh_details = this.ssh_details(cx);
let is_via_remote = self.remote_client.is_some();
let path: Option<Arc<Path>> = match &kind {
TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())),
TerminalKind::Task(spawn_task) => {
if let Some(cwd) = &spawn_task.cwd {
if ssh_details.is_some() {
if is_via_remote {
Some(Arc::from(cwd.as_ref()))
} else {
let cwd = cwd.to_string_lossy();
@@ -241,16 +178,14 @@ impl Project {
Some(Arc::from(Path::new(tilde_substituted.as_ref())))
}
} else {
this.active_project_directory(cx)
self.active_project_directory(cx)
}
}
};
let is_ssh_terminal = ssh_details.is_some();
let mut settings_location = None;
if let Some(path) = path.as_ref()
&& let Some((worktree, _)) = this.find_worktree(path, cx)
&& let Some((worktree, _)) = self.find_worktree(path, cx)
{
settings_location = Some(SettingsLocation {
worktree_id: worktree.read(cx).id(),
@@ -262,7 +197,7 @@ impl Project {
let (completion_tx, completion_rx) = bounded(1);
// Start with the environment that we might have inherited from the Zed CLI.
let mut env = this
let mut env = self
.environment
.read(cx)
.get_cli_environment()
@@ -271,14 +206,17 @@ impl Project {
// precedence.
env.extend(settings.env);
let local_path = if is_ssh_terminal { None } else { path.clone() };
let local_path = if is_via_remote { None } else { path.clone() };
let mut python_venv_activate_command = Task::ready(None);
let (spawn_task, shell) = match kind {
let remote_client = self.remote_client.clone();
let spawn_task;
let shell;
match kind {
TerminalKind::Shell(_) => {
if let Some(python_venv_directory) = &python_venv_directory {
python_venv_activate_command = this.python_activate_command(
python_venv_activate_command = self.python_activate_command(
python_venv_directory,
&settings.detect_venv,
&settings.shell,
@@ -286,63 +224,16 @@ impl Project {
);
}
match ssh_details {
Some(SshDetails {
host,
ssh_command,
envs,
path_style,
shell,
}) => {
log::debug!("Connecting to a remote server: {ssh_command:?}");
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
// to properly display colors.
// We do not have the luxury of assuming the host has it installed,
// so we set it to a default that does not break the highlighting via ssh.
env.entry("TERM".to_string())
.or_insert_with(|| "xterm-256color".to_string());
let (program, args) = wrap_for_ssh(
&shell,
&ssh_command,
None,
path.as_deref(),
env,
None,
path_style,
);
env = HashMap::default();
if let Some(envs) = envs {
env.extend(envs);
}
(
Option::<TaskState>::None,
Shell::WithArguments {
program,
args,
title_override: Some(format!("{} — Terminal", host).into()),
},
)
spawn_task = None;
shell = match remote_client {
Some(remote_client) => {
create_remote_shell(None, &mut env, path, remote_client, cx)?
}
None => (None, settings.shell),
}
None => settings.shell,
};
}
TerminalKind::Task(spawn_task) => {
let task_state = Some(TaskState {
id: spawn_task.id,
full_label: spawn_task.full_label,
label: spawn_task.label,
command_label: spawn_task.command_label,
hide: spawn_task.hide,
status: TaskStatus::Running,
show_summary: spawn_task.show_summary,
show_command: spawn_task.show_command,
show_rerun: spawn_task.show_rerun,
completion_rx,
});
env.extend(spawn_task.env);
TerminalKind::Task(task) => {
env.extend(task.env);
if let Some(venv_path) = &python_venv_directory {
env.insert(
@@ -351,41 +242,38 @@ impl Project {
);
}
match ssh_details {
Some(SshDetails {
host,
ssh_command,
envs,
path_style,
shell,
}) => {
log::debug!("Connecting to a remote server: {ssh_command:?}");
env.entry("TERM".to_string())
.or_insert_with(|| "xterm-256color".to_string());
let (program, args) = wrap_for_ssh(
&shell,
&ssh_command,
spawn_task
.command
.as_ref()
.map(|command| (command, &spawn_task.args)),
path.as_deref(),
env,
python_venv_directory.as_deref(),
path_style,
);
env = HashMap::default();
if let Some(envs) = envs {
env.extend(envs);
spawn_task = Some(TaskState {
id: task.id,
full_label: task.full_label,
label: task.label,
command_label: task.command_label,
hide: task.hide,
status: TaskStatus::Running,
show_summary: task.show_summary,
show_command: task.show_command,
show_rerun: task.show_rerun,
completion_rx,
});
shell = match remote_client {
Some(remote_client) => {
let path_style = remote_client.read(cx).path_style();
if let Some(venv_directory) = &python_venv_directory
&& let Ok(str) =
shlex::try_quote(venv_directory.to_string_lossy().as_ref())
{
let path =
RemotePathBuf::new(PathBuf::from(str.to_string()), path_style)
.to_string();
env.insert("PATH".into(), format!("{}:$PATH ", path));
}
(
task_state,
Shell::WithArguments {
program,
args,
title_override: Some(format!("{} — Terminal", host).into()),
},
)
create_remote_shell(
task.command.as_ref().map(|command| (command, &task.args)),
&mut env,
path,
remote_client,
cx,
)?
}
None => {
if let Some(venv_path) = &python_venv_directory {
@@ -393,18 +281,17 @@ impl Project {
.log_err();
}
let shell = if let Some(program) = spawn_task.command {
if let Some(program) = task.command {
Shell::WithArguments {
program,
args: spawn_task.args,
args: task.args,
title_override: None,
}
} else {
Shell::System
};
(task_state, shell)
}
}
}
};
}
};
TerminalBuilder::new(
@@ -416,7 +303,7 @@ impl Project {
settings.cursor_shape.unwrap_or_default(),
settings.alternate_scroll,
settings.max_scroll_history_lines,
is_ssh_terminal,
is_via_remote,
cx.entity_id().as_u64(),
completion_tx,
cx,
@@ -424,7 +311,7 @@ impl Project {
.map(|builder| {
let terminal_handle = cx.new(|cx| builder.subscribe(cx));
this.terminals
self.terminals
.local_handles
.push(terminal_handle.downgrade());
@@ -442,7 +329,7 @@ impl Project {
})
.detach();
this.activate_python_virtual_environment(
self.activate_python_virtual_environment(
python_venv_activate_command,
&terminal_handle,
cx,
@@ -652,62 +539,42 @@ impl Project {
}
}
pub fn wrap_for_ssh(
shell: &str,
ssh_command: &SshCommand,
command: Option<(&String, &Vec<String>)>,
path: Option<&Path>,
env: HashMap<String, String>,
venv_directory: Option<&Path>,
path_style: PathStyle,
) -> (String, Vec<String>) {
let to_run = if let Some((command, args)) = command {
let command: Option<Cow<str>> = shlex::try_quote(command).ok();
let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok());
command.into_iter().chain(args).join(" ")
} else {
format!("exec {shell} -l")
fn create_remote_shell(
spawn_command: Option<(&String, &Vec<String>)>,
env: &mut HashMap<String, String>,
working_directory: Option<Arc<Path>>,
remote_client: Entity<RemoteClient>,
cx: &mut App,
) -> Result<Shell> {
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
// to properly display colors.
// We do not have the luxury of assuming the host has it installed,
// so we set it to a default that does not break the highlighting via ssh.
env.entry("TERM".to_string())
.or_insert_with(|| "xterm-256color".to_string());
let (program, args) = match spawn_command {
Some((program, args)) => (Some(program.clone()), args),
None => (None, &Vec::new()),
};
let mut env_changes = String::new();
for (k, v) in env.iter() {
if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) {
env_changes.push_str(&format!("{}={} ", k, v));
}
}
if let Some(venv_directory) = venv_directory
&& let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref())
{
let path = RemotePathBuf::new(PathBuf::from(str.to_string()), path_style).to_string();
env_changes.push_str(&format!("PATH={}:$PATH ", path));
}
let command = remote_client.read(cx).build_command(
program,
args.as_slice(),
env,
working_directory.map(|path| path.display().to_string()),
None,
)?;
*env = command.env;
let commands = if let Some(path) = path {
let path = RemotePathBuf::new(path.to_path_buf(), path_style).to_string();
// shlex will wrap the command in single quotes (''), disabling ~ expansion,
// replace ith with something that works
let tilde_prefix = "~/";
if path.starts_with(tilde_prefix) {
let trimmed_path = path
.trim_start_matches("/")
.trim_start_matches("~")
.trim_start_matches("/");
log::debug!("Connecting to a remote server: {:?}", command.program);
let host = remote_client.read(cx).connection_options().host;
format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}")
} else {
format!("cd \"{path}\"; {env_changes} {to_run}")
}
} else {
format!("cd; {env_changes} {to_run}")
};
let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&commands).unwrap());
let program = "ssh".to_string();
let mut args = ssh_command.arguments.clone();
args.push("-t".to_string());
args.push(shell_invocation);
(program, args)
Ok(Shell::WithArguments {
program: command.program,
args: command.args,
title_override: Some(format!("{} — Terminal", host).into()),
})
}
fn add_environment_path(env: &mut HashMap<String, String>, new_path: &Path) -> Result<()> {
+3 -3
View File
@@ -18,7 +18,7 @@ use gpui::{
use postage::oneshot;
use rpc::{
AnyProtoClient, ErrorExt, TypedEnvelope,
proto::{self, FromProto, SSH_PROJECT_ID, ToProto},
proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto},
};
use smol::{
channel::{Receiver, Sender},
@@ -278,7 +278,7 @@ impl WorktreeStore {
let path = RemotePathBuf::new(abs_path.into(), path_style);
let response = client
.request(proto::AddWorktree {
project_id: SSH_PROJECT_ID,
project_id: REMOTE_SERVER_PROJECT_ID,
path: path.to_proto(),
visible,
})
@@ -298,7 +298,7 @@ impl WorktreeStore {
let worktree = cx.update(|cx| {
Worktree::remote(
SSH_PROJECT_ID,
REMOTE_SERVER_PROJECT_ID,
0,
proto::WorktreeMetadata {
id: response.worktree_id,
+2 -2
View File
@@ -653,7 +653,7 @@ impl ProjectPanel {
let file_path = entry.path.clone();
let worktree_id = worktree.read(cx).id();
let entry_id = entry.id;
let is_via_ssh = project.read(cx).is_via_ssh();
let is_via_ssh = project.read(cx).is_via_remote_server();
workspace
.open_path_preview(
@@ -5301,7 +5301,7 @@ impl Render for ProjectPanel {
.on_action(cx.listener(Self::open_system))
.on_action(cx.listener(Self::open_in_terminal))
})
.when(project.is_via_ssh(), |el| {
.when(project.is_via_remote_server(), |el| {
el.on_action(cx.listener(Self::open_in_terminal))
})
.on_mouse_down(
+2 -2
View File
@@ -16,8 +16,8 @@ pub use typed_envelope::*;
include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
pub const SSH_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
pub const SSH_PROJECT_ID: u64 = 0;
pub const REMOTE_SERVER_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
pub const REMOTE_SERVER_PROJECT_ID: u64 = 0;
messages!(
(Ack, Foreground),
@@ -64,8 +64,8 @@ impl DisconnectedOverlay {
}
let handle = cx.entity().downgrade();
let ssh_connection_options = project.read(cx).ssh_connection_options(cx);
let host = if let Some(ssh_connection_options) = ssh_connection_options {
let remote_connection_options = project.read(cx).remote_connection_options(cx);
let host = if let Some(ssh_connection_options) = remote_connection_options {
Host::SshRemoteProject(ssh_connection_options)
} else {
Host::RemoteProject
+4 -4
View File
@@ -28,8 +28,8 @@ use paths::user_ssh_config_file;
use picker::Picker;
use project::Fs;
use project::Project;
use remote::ssh_session::ConnectionIdentifier;
use remote::{SshConnectionOptions, SshRemoteClient};
use remote::remote_client::ConnectionIdentifier;
use remote::{RemoteClient, SshConnectionOptions};
use settings::Settings;
use settings::SettingsStore;
use settings::update_settings_file;
@@ -69,7 +69,7 @@ pub struct RemoteServerProjects {
mode: Mode,
focus_handle: FocusHandle,
workspace: WeakEntity<Workspace>,
retained_connections: Vec<Entity<SshRemoteClient>>,
retained_connections: Vec<Entity<RemoteClient>>,
ssh_config_updates: Task<()>,
ssh_config_servers: BTreeSet<SharedString>,
create_new_window: bool,
@@ -597,7 +597,7 @@ impl RemoteServerProjects {
let (path_style, project) = cx.update(|_, cx| {
(
session.read(cx).path_style(),
project::Project::ssh(
project::Project::remote(
session,
app_state.client.clone(),
app_state.node_runtime.clone(),
+10 -9
View File
@@ -15,8 +15,9 @@ use gpui::{
use language::CursorShape;
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use release_channel::ReleaseChannel;
use remote::ssh_session::{ConnectionIdentifier, SshPortForwardOption};
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
use remote::{
ConnectionIdentifier, RemoteClient, RemotePlatform, SshConnectionOptions, SshPortForwardOption,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
@@ -451,7 +452,7 @@ pub struct SshClientDelegate {
known_password: Option<String>,
}
impl remote::SshClientDelegate for SshClientDelegate {
impl remote::RemoteClientDelegate for SshClientDelegate {
fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, cx: &mut AsyncApp) {
let mut known_password = self.known_password.clone();
if let Some(password) = known_password.take() {
@@ -473,7 +474,7 @@ impl remote::SshClientDelegate for SshClientDelegate {
fn download_server_binary_locally(
&self,
platform: SshPlatform,
platform: RemotePlatform,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncApp,
@@ -503,7 +504,7 @@ impl remote::SshClientDelegate for SshClientDelegate {
fn get_download_params(
&self,
platform: SshPlatform,
platform: RemotePlatform,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncApp,
@@ -543,13 +544,13 @@ pub fn connect_over_ssh(
ui: Entity<SshPrompt>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Option<Entity<SshRemoteClient>>>> {
) -> Task<Result<Option<Entity<RemoteClient>>>> {
let window = window.window_handle();
let known_password = connection_options.password.clone();
let (tx, rx) = oneshot::channel();
ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx));
remote::SshRemoteClient::new(
remote::RemoteClient::ssh(
unique_identifier,
connection_options,
rx,
@@ -681,9 +682,9 @@ pub async fn open_ssh_project(
window
.update(cx, |workspace, _, cx| {
if let Some(client) = workspace.project().read(cx).ssh_client() {
if let Some(client) = workspace.project().read(cx).remote_client() {
ExtensionStore::global(cx)
.update(cx, |store, cx| store.register_ssh_client(client, cx));
.update(cx, |store, cx| store.register_remote_client(client, cx));
}
})
.ok();
+10
View File
@@ -51,6 +51,16 @@ pub async fn write_message<S: AsyncWrite + Unpin>(
Ok(())
}
pub async fn write_size_prefixed_buffer<S: AsyncWrite + Unpin>(
stream: &mut S,
buffer: &mut Vec<u8>,
) -> Result<()> {
let len = buffer.len() as u32;
stream.write_all(len.to_le_bytes().as_slice()).await?;
stream.write_all(buffer).await?;
Ok(())
}
pub async fn read_message_raw<S: AsyncRead + Unpin>(
stream: &mut S,
buffer: &mut Vec<u8>,
+6 -4
View File
@@ -1,9 +1,11 @@
pub mod json_log;
pub mod protocol;
pub mod proxy;
pub mod ssh_session;
pub mod remote_client;
mod transport;
pub use ssh_session::{
ConnectionState, SshClientDelegate, SshConnectionOptions, SshInfo, SshPlatform,
SshRemoteClient, SshRemoteEvent,
pub use remote_client::{
ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent,
RemotePlatform,
};
pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
pub mod ssh;
File diff suppressed because it is too large Load Diff
+26 -26
View File
@@ -21,7 +21,7 @@ use project::{
};
use rpc::{
AnyProtoClient, TypedEnvelope,
proto::{self, SSH_PEER_ID, SSH_PROJECT_ID},
proto::{self, REMOTE_SERVER_PEER_ID, REMOTE_SERVER_PROJECT_ID},
};
use settings::initial_server_settings_content;
@@ -83,7 +83,7 @@ impl HeadlessProject {
let worktree_store = cx.new(|cx| {
let mut store = WorktreeStore::local(true, fs.clone());
store.shared(SSH_PROJECT_ID, session.clone(), cx);
store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
store
});
@@ -101,7 +101,7 @@ impl HeadlessProject {
let buffer_store = cx.new(|cx| {
let mut buffer_store = BufferStore::local(worktree_store.clone(), cx);
buffer_store.shared(SSH_PROJECT_ID, session.clone(), cx);
buffer_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
buffer_store
});
@@ -119,7 +119,7 @@ impl HeadlessProject {
breakpoint_store.clone(),
cx,
);
dap_store.shared(SSH_PROJECT_ID, session.clone(), cx);
dap_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
dap_store
});
@@ -131,7 +131,7 @@ impl HeadlessProject {
fs.clone(),
cx,
);
store.shared(SSH_PROJECT_ID, session.clone(), cx);
store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
store
});
@@ -154,7 +154,7 @@ impl HeadlessProject {
environment.clone(),
cx,
);
task_store.shared(SSH_PROJECT_ID, session.clone(), cx);
task_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
task_store
});
let settings_observer = cx.new(|cx| {
@@ -164,7 +164,7 @@ impl HeadlessProject {
task_store.clone(),
cx,
);
observer.shared(SSH_PROJECT_ID, session.clone(), cx);
observer.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
observer
});
@@ -185,7 +185,7 @@ impl HeadlessProject {
fs.clone(),
cx,
);
lsp_store.shared(SSH_PROJECT_ID, session.clone(), cx);
lsp_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
lsp_store
});
@@ -213,15 +213,15 @@ impl HeadlessProject {
);
// local_machine -> ssh handlers
session.subscribe_to_entity(SSH_PROJECT_ID, &worktree_store);
session.subscribe_to_entity(SSH_PROJECT_ID, &buffer_store);
session.subscribe_to_entity(SSH_PROJECT_ID, &cx.entity());
session.subscribe_to_entity(SSH_PROJECT_ID, &lsp_store);
session.subscribe_to_entity(SSH_PROJECT_ID, &task_store);
session.subscribe_to_entity(SSH_PROJECT_ID, &toolchain_store);
session.subscribe_to_entity(SSH_PROJECT_ID, &dap_store);
session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer);
session.subscribe_to_entity(SSH_PROJECT_ID, &git_store);
session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &worktree_store);
session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &buffer_store);
session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &cx.entity());
session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &lsp_store);
session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &task_store);
session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &toolchain_store);
session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &dap_store);
session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &settings_observer);
session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &git_store);
session.add_request_handler(cx.weak_entity(), Self::handle_list_remote_directory);
session.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata);
@@ -288,7 +288,7 @@ impl HeadlessProject {
} = event
{
cx.background_spawn(self.session.request(proto::UpdateBuffer {
project_id: SSH_PROJECT_ID,
project_id: REMOTE_SERVER_PROJECT_ID,
buffer_id: buffer.read(cx).remote_id().to_proto(),
operations: vec![serialize_operation(operation)],
}))
@@ -310,7 +310,7 @@ impl HeadlessProject {
} => {
self.session
.send(proto::UpdateLanguageServer {
project_id: SSH_PROJECT_ID,
project_id: REMOTE_SERVER_PROJECT_ID,
server_name: name.as_ref().map(|name| name.to_string()),
language_server_id: language_server_id.to_proto(),
variant: Some(message.clone()),
@@ -320,7 +320,7 @@ impl HeadlessProject {
LspStoreEvent::Notification(message) => {
self.session
.send(proto::Toast {
project_id: SSH_PROJECT_ID,
project_id: REMOTE_SERVER_PROJECT_ID,
notification_id: "lsp".to_string(),
message: message.clone(),
})
@@ -329,7 +329,7 @@ impl HeadlessProject {
LspStoreEvent::LanguageServerLog(language_server_id, log_type, message) => {
self.session
.send(proto::LanguageServerLog {
project_id: SSH_PROJECT_ID,
project_id: REMOTE_SERVER_PROJECT_ID,
language_server_id: language_server_id.to_proto(),
message: message.clone(),
log_type: Some(log_type.to_proto()),
@@ -338,7 +338,7 @@ impl HeadlessProject {
}
LspStoreEvent::LanguageServerPrompt(prompt) => {
let request = self.session.request(proto::LanguageServerPromptRequest {
project_id: SSH_PROJECT_ID,
project_id: REMOTE_SERVER_PROJECT_ID,
actions: prompt
.actions
.iter()
@@ -474,7 +474,7 @@ impl HeadlessProject {
let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
buffer_store.update(&mut cx, |buffer_store, cx| {
buffer_store
.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
.create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
.detach_and_log_err(cx);
})?;
@@ -500,7 +500,7 @@ impl HeadlessProject {
let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
buffer_store.update(&mut cx, |buffer_store, cx| {
buffer_store
.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
.create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
.detach_and_log_err(cx);
})?;
@@ -550,7 +550,7 @@ impl HeadlessProject {
buffer_store.update(cx, |buffer_store, cx| {
buffer_store
.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
.create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
.detach_and_log_err(cx);
});
@@ -586,7 +586,7 @@ impl HeadlessProject {
response.buffer_ids.push(buffer_id.to_proto());
buffer_store
.update(&mut cx, |buffer_store, cx| {
buffer_store.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
buffer_store.create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
})?
.await?;
}
@@ -22,7 +22,7 @@ use project::{
Project, ProjectPath,
search::{SearchQuery, SearchResult},
};
use remote::SshRemoteClient;
use remote::RemoteClient;
use serde_json::json;
use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content};
use smol::stream::StreamExt;
@@ -1119,7 +1119,7 @@ async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext)
buffer.edit([(ix..ix + 1, "100")], None, cx);
});
let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
let client = cx.read(|cx| project.read(cx).remote_client().unwrap());
client
.update(cx, |client, cx| client.simulate_disconnect(cx))
.detach();
@@ -1782,7 +1782,7 @@ pub async fn init_test(
});
init_logger();
let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
let (opts, ssh_server_client) = RemoteClient::fake_server(cx, server_cx);
let http_client = Arc::new(BlockedHttpClient);
let node_runtime = NodeRuntime::unavailable();
let languages = Arc::new(LanguageRegistry::new(cx.executor()));
@@ -1804,7 +1804,7 @@ pub async fn init_test(
)
});
let ssh = SshRemoteClient::fake_client(opts, cx).await;
let ssh = RemoteClient::fake_client(opts, cx).await;
let project = build_project(ssh, cx);
project
.update(cx, {
@@ -1819,7 +1819,7 @@ fn init_logger() {
zlog::init_test();
}
fn build_project(ssh: Entity<SshRemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
cx.update(|cx| {
if !cx.has_global::<SettingsStore>() {
let settings_store = SettingsStore::test(cx);
@@ -1845,5 +1845,5 @@ fn build_project(ssh: Entity<SshRemoteClient>, cx: &mut TestAppContext) -> Entit
language::init(cx);
});
cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, cx))
}
+6 -19
View File
@@ -19,14 +19,14 @@ use project::project_settings::ProjectSettings;
use proto::CrashReport;
use release_channel::{AppVersion, RELEASE_CHANNEL, ReleaseChannel};
use remote::SshRemoteClient;
use remote::RemoteClient;
use remote::{
json_log::LogRecord,
protocol::{read_message, write_message},
proxy::ProxyLaunchError,
};
use reqwest_client::ReqwestClient;
use rpc::proto::{self, Envelope, SSH_PROJECT_ID};
use rpc::proto::{self, Envelope, REMOTE_SERVER_PROJECT_ID};
use rpc::{AnyProtoClient, TypedEnvelope};
use settings::{Settings, SettingsStore, watch_config_file};
use smol::channel::{Receiver, Sender};
@@ -396,7 +396,7 @@ fn start_server(
})
.detach();
SshRemoteClient::proto_client_from_channels(incoming_rx, outgoing_tx, cx, "server")
RemoteClient::proto_client_from_channels(incoming_rx, outgoing_tx, cx, "server")
}
fn init_paths() -> anyhow::Result<()> {
@@ -867,34 +867,21 @@ where
R: AsyncRead + Unpin,
W: AsyncWrite + Unpin,
{
use remote::protocol::read_message_raw;
use remote::protocol::{read_message_raw, write_size_prefixed_buffer};
let mut buffer = Vec::new();
loop {
read_message_raw(&mut reader, &mut buffer)
.await
.with_context(|| format!("failed to read message from {}", socket_name))?;
write_size_prefixed_buffer(&mut writer, &mut buffer)
.await
.with_context(|| format!("failed to write message to {}", socket_name))?;
writer.flush().await?;
buffer.clear();
}
}
async fn write_size_prefixed_buffer<S: AsyncWrite + Unpin>(
stream: &mut S,
buffer: &mut Vec<u8>,
) -> Result<()> {
let len = buffer.len() as u32;
stream.write_all(len.to_le_bytes().as_slice()).await?;
stream.write_all(buffer).await?;
Ok(())
}
fn initialize_settings(
session: AnyProtoClient,
fs: Arc<dyn Fs>,
@@ -910,7 +897,7 @@ fn initialize_settings(
session
.send(proto::Toast {
project_id: SSH_PROJECT_ID,
project_id: REMOTE_SERVER_PROJECT_ID,
notification_id: "server-settings-failed".to_string(),
message: format!(
"Error in settings on remote host {:?}: {}",
@@ -922,7 +909,7 @@ fn initialize_settings(
} else {
session
.send(proto::HideToast {
project_id: SSH_PROJECT_ID,
project_id: REMOTE_SERVER_PROJECT_ID,
notification_id: "server-settings-failed".to_string(),
})
.log_err();
+8 -7
View File
@@ -36,7 +36,7 @@ impl ShellKind {
} else if program == "csh" {
ShellKind::Csh
} else {
// Someother shell detected, the user might install and use a
// Some other shell detected, the user might install and use a
// unix-like shell.
ShellKind::Posix
}
@@ -203,14 +203,15 @@ pub struct ShellBuilder {
impl ShellBuilder {
/// Create a new ShellBuilder as configured.
pub fn new(remote_system_shell: Option<&str>, shell: &Shell) -> Self {
let (program, args) = match shell {
Shell::System => match remote_system_shell {
Some(remote_shell) => (remote_shell.to_string(), Vec::new()),
None => (system_shell(), Vec::new()),
let (program, args) = match remote_system_shell {
Some(program) => (program.to_string(), Vec::new()),
None => match shell {
Shell::System => (system_shell(), Vec::new()),
Shell::Program(shell) => (shell.clone(), Vec::new()),
Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
},
Shell::Program(shell) => (shell.clone(), Vec::new()),
Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
};
let kind = ShellKind::new(&program);
Self {
program,
+1 -1
View File
@@ -1403,7 +1403,7 @@ impl InputHandler for TerminalInputHandler {
window.invalidate_character_coordinates();
let project = this.project().read(cx);
let telemetry = project.client().telemetry().clone();
telemetry.log_edit_event("terminal", project.is_via_ssh());
telemetry.log_edit_event("terminal", project.is_via_remote_server());
})
.ok();
}
+20 -9
View File
@@ -481,17 +481,28 @@ impl TerminalPanel {
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<WeakEntity<Terminal>>> {
let Ok((ssh_client, false)) = self.workspace.update(cx, |workspace, cx| {
let project = workspace.project().read(cx);
(
project.ssh_client().and_then(|it| it.read(cx).ssh_info()),
project.is_via_collab(),
)
}) else {
return Task::ready(Err(anyhow!("Project is not local")));
let remote_client = self
.workspace
.update(cx, |workspace, cx| {
let project = workspace.project().read(cx);
if project.is_via_collab() {
Err(anyhow!("cannot spawn tasks as a guest"))
} else {
Ok(project.remote_client())
}
})
.flatten();
let remote_client = match remote_client {
Ok(remote_client) => remote_client,
Err(e) => return Task::ready(Err(e)),
};
let builder = ShellBuilder::new(ssh_client.as_ref().map(|info| &*info.shell), &task.shell);
let remote_shell = remote_client
.as_ref()
.and_then(|remote_client| remote_client.read(cx).shell());
let builder = ShellBuilder::new(remote_shell.as_deref(), &task.shell);
let command_label = builder.command_label(&task.command_label);
let (command, args) = builder.build(task.command.clone(), &task.args);
+1 -1
View File
@@ -337,7 +337,7 @@ impl TitleBar {
let room = room.read(cx);
let project = self.project.read(cx);
let is_local = project.is_local() || project.is_via_ssh();
let is_local = project.is_local() || project.is_via_remote_server();
let is_shared = is_local && project.is_shared();
let is_muted = room.is_muted();
let muted_by_user = room.muted_by_user();
+6 -6
View File
@@ -299,8 +299,8 @@ impl TitleBar {
}
}
fn render_ssh_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let options = self.project.read(cx).ssh_connection_options(cx)?;
fn render_remote_project_connection(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let options = self.project.read(cx).remote_connection_options(cx)?;
let host: SharedString = options.connection_string().into();
let nickname = options
@@ -308,7 +308,7 @@ impl TitleBar {
.map(|nick| nick.into())
.unwrap_or_else(|| host.clone());
let (indicator_color, meta) = match self.project.read(cx).ssh_connection_state(cx)? {
let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? {
remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
remote::ConnectionState::HeartbeatMissed => (
@@ -324,7 +324,7 @@ impl TitleBar {
}
};
let icon_color = match self.project.read(cx).ssh_connection_state(cx)? {
let icon_color = match self.project.read(cx).remote_connection_state(cx)? {
remote::ConnectionState::Connecting => Color::Info,
remote::ConnectionState::Connected => Color::Default,
remote::ConnectionState::HeartbeatMissed => Color::Warning,
@@ -379,8 +379,8 @@ impl TitleBar {
}
pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
if self.project.read(cx).is_via_ssh() {
return self.render_ssh_project_host(cx);
if self.project.read(cx).is_via_remote_server() {
return self.render_remote_project_connection(cx);
}
if self.project.read(cx).is_disconnected(cx) {
+3 -1
View File
@@ -1924,7 +1924,9 @@ impl ShellExec {
let Some(range) = input_range else { return };
let mut process = project.read(cx).exec_in_shell(command, cx);
let Some(mut process) = project.read(cx).exec_in_shell(command, cx).log_err() else {
return;
};
process.stdout(Stdio::piped());
process.stderr(Stdio::piped());
+1 -1
View File
@@ -20,7 +20,7 @@ impl Workspace {
window: &mut Window,
cx: &mut Context<Self>,
) {
match self.project.read(cx).ssh_connection_state(cx) {
match self.project.read(cx).remote_connection_state(cx) {
None | Some(ConnectionState::Connected) => {}
Some(
ConnectionState::Connecting
+7 -7
View File
@@ -74,7 +74,7 @@ use project::{
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
};
use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIdentifier};
use remote::{RemoteClientDelegate, SshConnectionOptions, remote_client::ConnectionIdentifier};
use schemars::JsonSchema;
use serde::Deserialize;
use session::AppSession;
@@ -2084,7 +2084,7 @@ impl Workspace {
cx: &mut Context<Self>,
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
if self.project.read(cx).is_via_collab()
|| self.project.read(cx).is_via_ssh()
|| self.project.read(cx).is_via_remote_server()
|| !WorkspaceSettings::get_global(cx).use_system_path_prompts
{
let prompt = self.on_prompt_for_new_path.take().unwrap();
@@ -5249,7 +5249,7 @@ impl Workspace {
fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation {
let paths = PathList::new(&self.root_paths(cx));
if let Some(connection) = self.project.read(cx).ssh_connection_options(cx) {
if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
WorkspaceLocation::Location(
SerializedWorkspaceLocation::Ssh(SerializedSshConnection {
host: connection.host,
@@ -6917,7 +6917,7 @@ async fn join_channel_internal(
return None;
}
if (project.is_local() || project.is_via_ssh())
if (project.is_local() || project.is_via_remote_server())
&& project.visible_worktrees(cx).any(|tree| {
tree.read(cx)
.root_entry()
@@ -7263,7 +7263,7 @@ pub fn open_ssh_project_with_new_connection(
window: WindowHandle<Workspace>,
connection_options: SshConnectionOptions,
cancel_rx: oneshot::Receiver<()>,
delegate: Arc<dyn SshClientDelegate>,
delegate: Arc<dyn RemoteClientDelegate>,
app_state: Arc<AppState>,
paths: Vec<PathBuf>,
cx: &mut App,
@@ -7274,7 +7274,7 @@ pub fn open_ssh_project_with_new_connection(
let session = match cx
.update(|cx| {
remote::SshRemoteClient::new(
remote::RemoteClient::ssh(
ConnectionIdentifier::Workspace(workspace_id.0),
connection_options,
cancel_rx,
@@ -7289,7 +7289,7 @@ pub fn open_ssh_project_with_new_connection(
};
let project = cx.update(|cx| {
project::Project::ssh(
project::Project::remote(
session,
app_state.client.clone(),
app_state.node_runtime.clone(),
+2 -2
View File
@@ -220,10 +220,10 @@ pub fn init(
let installation_id = installation_id.clone();
let system_id = system_id.clone();
let Some(ssh_client) = project.ssh_client() else {
let Some(remote_client) = project.remote_client() else {
return;
};
ssh_client.update(cx, |client, cx| {
remote_client.update(cx, |client, cx| {
if !TelemetrySettings::get_global(cx).diagnostics {
return;
}
+2 -2
View File
@@ -918,7 +918,7 @@ fn register_actions(
capture_audio(workspace, window, cx);
});
if workspace.project().read(cx).is_via_ssh() {
if workspace.project().read(cx).is_via_remote_server() {
workspace.register_action({
move |workspace, _: &OpenServerSettings, window, cx| {
let open_server_settings = workspace
@@ -1543,7 +1543,7 @@ pub fn open_new_ssh_project_from_project(
cx: &mut Context<Workspace>,
) -> Task<anyhow::Result<()>> {
let app_state = workspace.app_state().clone();
let Some(ssh_client) = workspace.project().read(cx).ssh_client() else {
let Some(ssh_client) = workspace.project().read(cx).remote_client() else {
return Task::ready(Err(anyhow::anyhow!("Not an ssh project")));
};
let connection_options = ssh_client.read(cx).connection_options();