From 022e662815a8d5ce0938e529799be3777abda578 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 19 Jul 2024 17:25:28 -0700 Subject: [PATCH] Start work on showing progress when initializing ssh remoting --- crates/remote/src/ssh_session.rs | 10 +- crates/zed/src/zed.rs | 2 +- crates/zed/src/zed/open_listener.rs | 137 +++++++++++++-------- crates/zed/src/zed/password_prompt.rs | 69 ----------- crates/zed/src/zed/ssh_connection_modal.rs | 95 ++++++++++++++ 5 files changed, 190 insertions(+), 123 deletions(-) delete mode 100644 crates/zed/src/zed/password_prompt.rs create mode 100644 crates/zed/src/zed/ssh_connection_modal.rs diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 42f624aa1d06513cdb18628bca920374659a641d..e6d00ceaf4561690649db41f0e78b3af850a5b7a 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -85,6 +85,7 @@ pub trait SshClientDelegate { platform: SshPlatform, cx: &mut AsyncAppContext, ) -> oneshot::Receiver>; + fn set_status(&self, status: Option<&str>, cx: &mut AsyncAppContext); } type ResponseChannels = Mutex)>>>; @@ -104,9 +105,11 @@ impl SshSession { let remote_binary_path = delegate.remote_server_binary_path(cx)?; ensure_server_binary( &client_state, + &delegate, &local_binary_path, &remote_binary_path, version, + cx, ) .await?; @@ -590,9 +593,11 @@ async fn query_platform(session: &SshClientState) -> Result { async fn ensure_server_binary( session: &SshClientState, + delegate: &Arc, src_path: &Path, dst_path: &Path, version: SemanticVersion, + cx: &mut AsyncAppContext, ) -> Result<()> { let mut dst_path_gz = dst_path.to_path_buf(); dst_path_gz.set_extension("gz"); @@ -618,6 +623,7 @@ async fn ensure_server_binary( let server_mode = 0o755; let t0 = Instant::now(); + delegate.set_status(Some("uploading remote development server"), cx); log::info!("uploading remote development server ({}kb)", size / 1024); session .upload_file(src_path, &dst_path_gz) @@ -625,7 +631,7 @@ async fn ensure_server_binary( .context("failed to upload server binary")?; log::info!("uploaded remote development server in {:?}", t0.elapsed()); - log::info!("extracting remote development server"); + delegate.set_status(Some("extracting remote development server"), cx); run_cmd( session .ssh_command("gunzip") @@ -634,7 +640,7 @@ async fn ensure_server_binary( ) .await?; - log::info!("unzipping remote development server"); + delegate.set_status(Some("unzipping remote development server"), cx); run_cmd( session .ssh_command("chmod") diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 771125ebc222b762013a88a493b1f3e956d6d8a7..75cf126b60aeb85ab2e8987a08120d7e2d5656f7 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -5,7 +5,7 @@ pub(crate) mod linux_prompts; #[cfg(not(target_os = "linux"))] pub(crate) mod only_instance; mod open_listener; -mod password_prompt; +mod ssh_connection_modal; pub use app_menus::*; use breadcrumbs::Breadcrumbs; diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 37ebb3b52cf3b9021efbe9cf308c52a0063a76ab..f3fa2e95d4b55cf546be49eae5035564e0d42be9 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -1,4 +1,6 @@ -use crate::{handle_open_request, init_headless, init_ui, zed::password_prompt::PasswordPrompt}; +use crate::{ + handle_open_request, init_headless, init_ui, zed::ssh_connection_modal::SshConnectionModal, +}; use anyhow::{anyhow, Context, Result}; use auto_update::AutoUpdater; use cli::{ipc, IpcHandshake}; @@ -12,7 +14,7 @@ use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::channel::{mpsc, oneshot}; use futures::{FutureExt, SinkExt, StreamExt}; use gpui::{ - AppContext, AsyncAppContext, Global, SemanticVersion, VisualContext as _, WindowHandle, + AppContext, AsyncAppContext, Global, SemanticVersion, View, VisualContext as _, WindowHandle, }; use language::{Bias, Point}; use release_channel::{AppVersion, ReleaseChannel}; @@ -155,8 +157,10 @@ impl OpenListener { } } +#[derive(Clone)] struct SshClientDelegate { window: WindowHandle, + modal: View, known_password: Option, } @@ -168,27 +172,34 @@ impl remote::SshClientDelegate for SshClientDelegate { ) -> oneshot::Receiver> { let (tx, rx) = oneshot::channel(); let mut known_password = self.known_password.clone(); - self.window - .update(cx, |workspace, cx| { - cx.activate_window(); - if let Some(password) = known_password.take() { - tx.send(Ok(password)).ok(); - } else { - workspace.toggle_modal(cx, |cx| PasswordPrompt::new(prompt, tx, cx)); - } - }) - .ok(); + if let Some(password) = known_password.take() { + tx.send(Ok(password)).ok(); + } else { + self.window + .update(cx, |_, cx| { + self.modal.update(cx, |modal, cx| { + modal.set_prompt(prompt, tx, cx); + }); + }) + .ok(); + } rx } + fn set_status(&self, status: Option<&str>, cx: &mut AsyncAppContext) { + self.update_status(status, cx) + } + fn get_server_binary( &self, platform: SshPlatform, cx: &mut AsyncAppContext, ) -> oneshot::Receiver> { let (tx, rx) = oneshot::channel(); + let this = self.clone(); cx.spawn(|mut cx| async move { - tx.send(get_server_binary(platform, &mut cx).await).ok(); + tx.send(this.get_server_binary_impl(platform, &mut cx).await) + .ok(); }) .detach(); rx @@ -200,48 +211,63 @@ impl remote::SshClientDelegate for SshClientDelegate { } } -async fn get_server_binary( - platform: SshPlatform, - cx: &mut AsyncAppContext, -) -> Result<(PathBuf, SemanticVersion)> { - let (version, release_channel) = - cx.update(|cx| (AppVersion::global(cx), ReleaseChannel::global(cx)))?; - - // In dev mode, build the remote server binary from source - #[cfg(debug_assertions)] - if crate::stdout_is_a_pty() - && release_channel == ReleaseChannel::Dev - && platform.arch == std::env::consts::ARCH - && platform.os == std::env::consts::OS - { - use smol::process::{Command, Stdio}; - - log::info!("building remote server binary from source"); - run_cmd(Command::new("cargo").args(["build", "--package", "remote_server"])).await?; - run_cmd(Command::new("strip").args(["target/debug/remote_server"])).await?; - run_cmd(Command::new("gzip").args(["-9", "-f", "target/debug/remote_server"])).await?; - - let path = std::env::current_dir()?.join("target/debug/remote_server.gz"); - return Ok((path, version)); - - async fn run_cmd(command: &mut Command) -> Result<()> { - let output = command.stderr(Stdio::inherit()).output().await?; - if !output.status.success() { - Err(anyhow!("failed to run command: {:?}", command))?; +impl SshClientDelegate { + fn update_status(&self, status: Option<&str>, cx: &mut AsyncAppContext) { + self.window + .update(cx, |_, cx| { + self.modal.update(cx, |modal, cx| { + modal.set_status(status.map(|s| s.to_string()), cx); + }); + }) + .ok(); + } + + async fn get_server_binary_impl( + &self, + platform: SshPlatform, + cx: &mut AsyncAppContext, + ) -> Result<(PathBuf, SemanticVersion)> { + let (version, release_channel) = + cx.update(|cx| (AppVersion::global(cx), ReleaseChannel::global(cx)))?; + + // In dev mode, build the remote server binary from source + #[cfg(debug_assertions)] + if crate::stdout_is_a_pty() + && release_channel == ReleaseChannel::Dev + && platform.arch == std::env::consts::ARCH + && platform.os == std::env::consts::OS + { + use smol::process::{Command, Stdio}; + + self.update_status(Some("building remote server binary from source"), cx); + log::info!("building remote server binary from source"); + run_cmd(Command::new("cargo").args(["build", "--package", "remote_server"])).await?; + run_cmd(Command::new("strip").args(["target/debug/remote_server"])).await?; + run_cmd(Command::new("gzip").args(["-9", "-f", "target/debug/remote_server"])).await?; + + let path = std::env::current_dir()?.join("target/debug/remote_server.gz"); + return Ok((path, version)); + + async fn run_cmd(command: &mut Command) -> Result<()> { + let output = command.stderr(Stdio::inherit()).output().await?; + if !output.status.success() { + Err(anyhow!("failed to run command: {:?}", command))?; + } + Ok(()) } - Ok(()) } - } - let binary_path = AutoUpdater::get_latest_remote_server_release( - platform.os, - platform.arch, - release_channel, - cx, - ) - .await?; + self.update_status(Some("checking for latest version of remote server"), cx); + let binary_path = AutoUpdater::get_latest_remote_server_release( + platform.os, + platform.arch, + release_channel, + cx, + ) + .await?; - Ok((binary_path, version)) + Ok((binary_path, version)) + } } #[cfg(target_os = "linux")] @@ -315,12 +341,21 @@ pub async fn open_ssh_paths( cx.new_view(|cx| Workspace::new(None, project, app_state.clone(), cx)) })?; + let modal = window.update(cx, |workspace, cx| { + cx.activate_window(); + workspace.toggle_modal(cx, |cx| { + SshConnectionModal::new(connection_info.host.clone(), cx) + }); + workspace.active_modal::(cx).unwrap() + })?; + let session = remote::SshSession::client( connection_info.username, connection_info.host, connection_info.port, Arc::new(SshClientDelegate { window, + modal, known_password: connection_info.password, }), cx, diff --git a/crates/zed/src/zed/password_prompt.rs b/crates/zed/src/zed/password_prompt.rs deleted file mode 100644 index c2ad7c1e7ee5d149ff59574db2f192b741ad35c8..0000000000000000000000000000000000000000 --- a/crates/zed/src/zed/password_prompt.rs +++ /dev/null @@ -1,69 +0,0 @@ -use anyhow::Result; -use editor::Editor; -use futures::channel::oneshot; -use gpui::{ - px, DismissEvent, EventEmitter, FocusableView, ParentElement as _, Render, SharedString, View, -}; -use ui::{v_flex, InteractiveElement, Label, Styled, StyledExt as _, ViewContext, VisualContext}; -use workspace::ModalView; - -pub struct PasswordPrompt { - prompt: SharedString, - tx: Option>>, - editor: View, -} - -impl PasswordPrompt { - pub fn new( - prompt: String, - tx: oneshot::Sender>, - cx: &mut ViewContext, - ) -> Self { - Self { - prompt: SharedString::from(prompt), - tx: Some(tx), - editor: cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); - editor.set_redact_all(true, cx); - editor - }), - } - } - - fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - let text = self.editor.read(cx).text(cx); - if let Some(tx) = self.tx.take() { - tx.send(Ok(text)).ok(); - }; - cx.emit(DismissEvent) - } - - fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(DismissEvent) - } -} - -impl Render for PasswordPrompt { - fn render(&mut self, cx: &mut ui::ViewContext) -> impl ui::IntoElement { - v_flex() - .key_context("PasswordPrompt") - .elevation_3(cx) - .p_4() - .gap_2() - .on_action(cx.listener(Self::dismiss)) - .on_action(cx.listener(Self::confirm)) - .w(px(400.)) - .child(Label::new(self.prompt.clone())) - .child(self.editor.clone()) - } -} - -impl FocusableView for PasswordPrompt { - fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { - self.editor.focus_handle(cx) - } -} - -impl EventEmitter for PasswordPrompt {} - -impl ModalView for PasswordPrompt {} diff --git a/crates/zed/src/zed/ssh_connection_modal.rs b/crates/zed/src/zed/ssh_connection_modal.rs new file mode 100644 index 0000000000000000000000000000000000000000..d15ba3f89cf9ede211ea6f3ec853575d8269b41e --- /dev/null +++ b/crates/zed/src/zed/ssh_connection_modal.rs @@ -0,0 +1,95 @@ +use anyhow::Result; +use editor::Editor; +use futures::channel::oneshot; +use gpui::{ + px, DismissEvent, EventEmitter, FocusableView, ParentElement as _, Render, SharedString, View, +}; +use ui::{ + v_flex, FluentBuilder as _, InteractiveElement, Label, LabelCommon, Styled, StyledExt as _, + ViewContext, VisualContext, +}; +use workspace::ModalView; + +pub struct SshConnectionModal { + host: SharedString, + status: Option, + prompt: Option<(SharedString, oneshot::Sender>)>, + editor: View, +} + +impl SshConnectionModal { + pub fn new(host: String, cx: &mut ViewContext) -> Self { + Self { + host: host.into(), + prompt: None, + status: None, + editor: cx.new_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_redact_all(true, cx); + editor + }), + } + } + + pub fn set_prompt( + &mut self, + prompt: String, + tx: oneshot::Sender>, + cx: &mut ViewContext, + ) { + self.prompt = Some((prompt.into(), tx)); + self.status.take(); + cx.focus_view(&self.editor); + cx.notify(); + } + + pub fn set_status(&mut self, status: Option, cx: &mut ViewContext) { + self.status = status.map(|s| s.into()); + cx.notify(); + } + + fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + let text = self.editor.read(cx).text(cx); + if let Some((_, tx)) = self.prompt.take() { + tx.send(Ok(text)).ok(); + }; + // cx.emit(DismissEvent) + } + + fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + if self.prompt.is_some() { + cx.emit(DismissEvent) + } + } +} + +impl Render for SshConnectionModal { + fn render(&mut self, cx: &mut ui::ViewContext) -> impl ui::IntoElement { + v_flex() + .key_context("PasswordPrompt") + .elevation_3(cx) + .p_4() + .gap_2() + .on_action(cx.listener(Self::dismiss)) + .on_action(cx.listener(Self::confirm)) + .w(px(400.)) + .child(Label::new(format!("SSH: {}", self.host)).size(ui::LabelSize::Large)) + .when_some(self.status.as_ref(), |el, status| { + el.child(Label::new(status.clone())) + }) + .when_some(self.prompt.as_ref(), |el, prompt| { + el.child(Label::new(prompt.0.clone())) + .child(self.editor.clone()) + }) + } +} + +impl FocusableView for SshConnectionModal { + fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { + self.editor.focus_handle(cx) + } +} + +impl EventEmitter for SshConnectionModal {} + +impl ModalView for SshConnectionModal {}