diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 4500e34aabbc3146e71c82c5b3cf42f875311ae4..dac6db1b12c38a967c3abd05cf34a96a44f77b08 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -98,7 +98,7 @@ impl AcpConnection { let stdout = child.stdout.take().context("Failed to take stdout")?; let stdin = child.stdin.take().context("Failed to take stdin")?; let stderr = child.stderr.take().context("Failed to take stderr")?; - log::info!( + log::debug!( "Spawning external agent server: {:?}, {:?}", command.path, command.args diff --git a/crates/cli/README.md b/crates/cli/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ac4384e2566685cb57601a833e25ed783197fb5f --- /dev/null +++ b/crates/cli/README.md @@ -0,0 +1,15 @@ +# Cli + +## Testing + +You can test your changes to the `cli` crate by first building the main zed binary: + +``` +cargo build -p zed +``` + +And then building and running the `cli` crate with the following parameters: + +``` + cargo run -p cli -- --zed ./target/debug/zed.exe +``` diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index f3c15c25e8ec9044894ed1030ffdeb72e319fbb7..4c25cf1c9d701369c7ce18a1cb70b8073da161e5 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -155,6 +155,7 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result { } fn parse_path_in_wsl(source: &str, wsl: &str) -> Result { + let mut source = PathWithPosition::parse_str(source); let mut command = util::command::new_std_command("wsl.exe"); let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') { @@ -173,19 +174,17 @@ fn parse_path_in_wsl(source: &str, wsl: &str) -> Result { let output = command .arg("--distribution") .arg(distro_name) + .arg("--exec") .arg("wslpath") .arg("-m") - .arg(source) + .arg(&source.path) .output()?; let result = String::from_utf8_lossy(&output.stdout); let prefix = format!("//wsl.localhost/{}", distro_name); + source.path = Path::new(result.trim().strip_prefix(&prefix).unwrap_or(&result)).to_owned(); - Ok(result - .trim() - .strip_prefix(&prefix) - .unwrap_or(&result) - .to_string()) + Ok(source.to_string(|path| path.to_string_lossy().into_owned())) } fn main() -> Result<()> { diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs index 4431c49a2d28ccfc5d799f3646cb9f46714183f1..8744bacf420b28ccb38c96dc949515e6e6ebadaf 100644 --- a/crates/recent_projects/src/remote_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -1,4 +1,7 @@ -use std::{path::PathBuf, sync::Arc}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use anyhow::{Context as _, Result}; use askpass::EncryptedPassword; @@ -12,11 +15,11 @@ use gpui::{ TextStyleRefinement, WeakEntity, }; -use language::CursorShape; +use language::{CursorShape, Point}; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use release_channel::ReleaseChannel; use remote::{ - ConnectionIdentifier, RemoteClient, RemoteConnectionOptions, RemotePlatform, + ConnectionIdentifier, RemoteClient, RemoteConnection, RemoteConnectionOptions, RemotePlatform, SshConnectionOptions, }; pub use settings::SshConnection; @@ -26,6 +29,7 @@ use ui::{ ActiveTheme, Color, CommonAnimationExt, Context, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, Styled, Window, prelude::*, }; +use util::paths::PathWithPosition; use workspace::{AppState, ModalView, Workspace}; pub struct SshSettings { @@ -533,34 +537,6 @@ impl RemoteClientDelegate { } } -pub fn connect_over_ssh( - unique_identifier: ConnectionIdentifier, - connection_options: SshConnectionOptions, - ui: Entity, - window: &mut Window, - cx: &mut App, -) -> Task>>> { - let window = window.window_handle(); - let known_password = connection_options - .password - .as_deref() - .and_then(|pw| EncryptedPassword::try_from(pw).ok()); - let (tx, rx) = oneshot::channel(); - ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx)); - - remote::RemoteClient::ssh( - unique_identifier, - connection_options, - rx, - Arc::new(RemoteClientDelegate { - window, - ui: ui.downgrade(), - known_password, - }), - cx, - ) -} - pub fn connect( unique_identifier: ConnectionIdentifier, connection_options: RemoteConnectionOptions, @@ -579,17 +555,17 @@ pub fn connect( let (tx, rx) = oneshot::channel(); ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx)); - remote::RemoteClient::new( - unique_identifier, - connection_options, - rx, - Arc::new(RemoteClientDelegate { - window, - ui: ui.downgrade(), - known_password, - }), - cx, - ) + let delegate = Arc::new(RemoteClientDelegate { + window, + ui: ui.downgrade(), + known_password, + }); + + cx.spawn(async move |cx| { + let connection = remote::connect(connection_options, delegate.clone(), cx).await?; + cx.update(|cx| remote::RemoteClient::new(unique_identifier, connection, rx, delegate, cx))? + .await + }) } pub async fn open_remote_project( @@ -604,6 +580,7 @@ pub async fn open_remote_project( } else { let workspace_position = cx .update(|cx| { + // todo: These paths are wrong they may have column and line information workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx) })? .await @@ -671,11 +648,16 @@ pub async fn open_remote_project( let Some(delegate) = delegate else { break }; - let did_open_project = cx + let remote_connection = + remote::connect(connection_options.clone(), delegate.clone(), cx).await?; + let (paths, paths_with_positions) = + determine_paths_with_positions(&remote_connection, paths.clone()).await; + + let opened_items = cx .update(|cx| { workspace::open_remote_project_with_new_connection( window, - connection_options.clone(), + remote_connection, cancel_rx, delegate.clone(), app_state.clone(), @@ -693,25 +675,51 @@ pub async fn open_remote_project( }) .ok(); - if let Err(e) = did_open_project { - log::error!("Failed to open project: {e:?}"); - let response = window - .update(cx, |_, window, cx| { - window.prompt( - PromptLevel::Critical, - match connection_options { - RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH", - RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL", - }, - Some(&e.to_string()), - &["Retry", "Ok"], - cx, - ) - })? - .await; - - if response == Ok(0) { - continue; + match opened_items { + Err(e) => { + log::error!("Failed to open project: {e:?}"); + let response = window + .update(cx, |_, window, cx| { + window.prompt( + PromptLevel::Critical, + match connection_options { + RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH", + RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL", + }, + Some(&e.to_string()), + &["Retry", "Ok"], + cx, + ) + })? + .await; + if response == Ok(0) { + continue; + } + } + Ok(items) => { + for (item, path) in items.into_iter().zip(paths_with_positions) { + let Some(item) = item else { + continue; + }; + let Some(row) = path.row else { + continue; + }; + if let Some(active_editor) = item.downcast::() { + window + .update(cx, |_, window, cx| { + active_editor.update(cx, |editor, cx| { + let row = row.saturating_sub(1); + let col = path.column.unwrap_or(0).saturating_sub(1); + editor.go_to_singleton_buffer_point( + Point::new(row, col), + window, + cx, + ); + }); + }) + .ok(); + } + } } } @@ -730,3 +738,44 @@ pub async fn open_remote_project( // Already showed the error to the user Ok(()) } + +pub(crate) async fn determine_paths_with_positions( + remote_connection: &Arc, + mut paths: Vec, +) -> (Vec, Vec) { + let mut paths_with_positions = Vec::::new(); + for path in &mut paths { + if let Some(path_str) = path.to_str() { + let path_with_position = PathWithPosition::parse_str(&path_str); + if path_with_position.row.is_some() { + if !path_exists(&remote_connection, &path).await { + *path = path_with_position.path.clone(); + paths_with_positions.push(path_with_position); + continue; + } + } + } + paths_with_positions.push(PathWithPosition::from_path(path.clone())) + } + (paths, paths_with_positions) +} + +async fn path_exists(connection: &Arc, path: &Path) -> bool { + let Ok(command) = connection.build_command( + Some("test".to_string()), + &["-e".to_owned(), path.to_string_lossy().to_string()], + &Default::default(), + None, + None, + ) else { + return false; + }; + let Ok(mut child) = util::command::new_smol_command(command.program) + .args(command.args) + .envs(command.env) + .spawn() + else { + return false; + }; + child.status().await.is_ok_and(|status| status.success()) +} diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 871be06684cba0a0cca51b9cd4f890925a8057db..2596a3d41604ac3710b9d5302718c18b2f948b4f 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1,7 +1,8 @@ use crate::{ remote_connections::{ Connection, RemoteConnectionModal, RemoteConnectionPrompt, SshConnection, - SshConnectionHeader, SshSettings, connect, connect_over_ssh, open_remote_project, + SshConnectionHeader, SshSettings, connect, determine_paths_with_positions, + open_remote_project, }, ssh_config::parse_ssh_config_hosts, }; @@ -13,6 +14,7 @@ use gpui::{ FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task, WeakEntity, Window, canvas, }; +use language::Point; use log::info; use paths::{global_ssh_config_file, user_ssh_config_file}; use picker::Picker; @@ -233,6 +235,15 @@ impl ProjectPicker { .read_with(cx, |workspace, _| workspace.app_state().clone()) .ok()?; + let remote_connection = project + .read_with(cx, |project, cx| { + project.remote_client()?.read(cx).connection() + }) + .ok()??; + + let (paths, paths_with_positions) = + determine_paths_with_positions(&remote_connection, paths).await; + cx.update(|_, cx| { let fs = app_state.fs.clone(); update_settings_file(fs, cx, { @@ -278,12 +289,38 @@ impl ProjectPicker { }) .log_err()?; - open_remote_project_with_existing_connection( + let items = open_remote_project_with_existing_connection( connection, project, paths, app_state, window, cx, ) .await .log_err(); + if let Some(items) = items { + for (item, path) in items.into_iter().zip(paths_with_positions) { + let Some(item) = item else { + continue; + }; + let Some(row) = path.row else { + continue; + }; + if let Some(active_editor) = item.downcast::() { + window + .update(cx, |_, window, cx| { + active_editor.update(cx, |editor, cx| { + let row = row.saturating_sub(1); + let col = path.column.unwrap_or(0).saturating_sub(1); + editor.go_to_singleton_buffer_point( + Point::new(row, col), + window, + cx, + ); + }); + }) + .ok(); + } + } + } + this.update(cx, |_, cx| { cx.emit(DismissEvent); }) @@ -671,9 +708,9 @@ impl RemoteServerProjects { ) }); - let connection = connect_over_ssh( + let connection = connect( ConnectionIdentifier::setup(), - connection_options.clone(), + RemoteConnectionOptions::Ssh(connection_options.clone()), ssh_prompt.clone(), window, cx, diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs index 74d45b1a696ff1a02a9f2b4d9afc3844f82196cd..62fe40f7649b9a1f8e4697a5c6b4c7d1690715e4 100644 --- a/crates/remote/src/remote.rs +++ b/crates/remote/src/remote.rs @@ -6,7 +6,7 @@ mod transport; pub use remote_client::{ ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent, - RemoteConnectionOptions, RemotePlatform, + RemoteConnection, RemoteConnectionOptions, RemotePlatform, connect, }; pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption}; pub use transport::wsl::WslConnectionOptions; diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index 0fea4726e7aafb5f3f04614b76f1ed5dd2d0a965..e9eafa25b0467f29d1dd12816aa17d65b94bf1d4 100644 --- a/crates/remote/src/remote_client.rs +++ b/crates/remote/src/remote_client.rs @@ -93,7 +93,7 @@ const MAX_RECONNECT_ATTEMPTS: usize = 3; enum State { Connecting, Connected { - ssh_connection: Arc, + remote_connection: Arc, delegate: Arc, multiplex_task: Task>, @@ -137,7 +137,10 @@ impl fmt::Display for State { impl State { fn remote_connection(&self) -> Option> { match self { - Self::Connected { ssh_connection, .. } => Some(ssh_connection.clone()), + Self::Connected { + remote_connection: ssh_connection, + .. + } => Some(ssh_connection.clone()), Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection.clone()), Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection.clone()), _ => None, @@ -181,7 +184,7 @@ impl State { heartbeat_task, .. } => Self::Connected { - ssh_connection, + remote_connection: ssh_connection, delegate, multiplex_task, heartbeat_task, @@ -193,7 +196,7 @@ impl State { fn heartbeat_missed(self) -> Self { match self { Self::Connected { - ssh_connection, + remote_connection: ssh_connection, delegate, multiplex_task, heartbeat_task, @@ -260,8 +263,8 @@ pub enum RemoteClientEvent { impl EventEmitter for RemoteClient {} -// Identifies the socket on the remote server so that reconnects -// can re-join the same project. +/// Identifies the socket on the remote server so that reconnects +/// can re-join the same project. pub enum ConnectionIdentifier { Setup(u64), Workspace(i64), @@ -294,26 +297,24 @@ impl ConnectionIdentifier { } } -impl RemoteClient { - pub fn ssh( - unique_identifier: ConnectionIdentifier, - connection_options: SshConnectionOptions, - cancellation: oneshot::Receiver<()>, - delegate: Arc, - cx: &mut App, - ) -> Task>>> { - Self::new( - unique_identifier, - RemoteConnectionOptions::Ssh(connection_options), - cancellation, - delegate, - cx, - ) - } +pub async fn connect( + connection_options: RemoteConnectionOptions, + delegate: Arc, + cx: &mut AsyncApp, +) -> Result> { + cx.update(|cx| { + cx.update_default_global(|pool: &mut ConnectionPool, cx| { + pool.connect(connection_options.clone(), delegate.clone(), cx) + }) + })? + .await + .map_err(|e| e.cloned()) +} +impl RemoteClient { pub fn new( unique_identifier: ConnectionIdentifier, - connection_options: RemoteConnectionOptions, + remote_connection: Arc, cancellation: oneshot::Receiver<()>, delegate: Arc, cx: &mut App, @@ -328,25 +329,16 @@ impl RemoteClient { let client = cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?; - let ssh_connection = cx - .update(|cx| { - cx.update_default_global(|pool: &mut ConnectionPool, cx| { - pool.connect(connection_options.clone(), delegate.clone(), cx) - }) - })? - .await - .map_err(|e| e.cloned())?; - - let path_style = ssh_connection.path_style(); + let path_style = remote_connection.path_style(); let this = cx.new(|_| Self { client: client.clone(), unique_identifier: unique_identifier.clone(), - connection_options, + connection_options: remote_connection.connection_options(), path_style, state: Some(State::Connecting), })?; - let io_task = ssh_connection.start_proxy( + let io_task = remote_connection.start_proxy( unique_identifier, false, incoming_tx, @@ -402,7 +394,7 @@ impl RemoteClient { this.update(cx, |this, _| { this.state = Some(State::Connected { - ssh_connection, + remote_connection, delegate, multiplex_task, heartbeat_task, @@ -441,7 +433,7 @@ impl RemoteClient { let State::Connected { multiplex_task, heartbeat_task, - ssh_connection, + remote_connection: ssh_connection, delegate, } = state else { @@ -488,7 +480,7 @@ impl RemoteClient { let state = self.state.take().unwrap(); let (attempts, remote_connection, delegate) = match state { State::Connected { - ssh_connection, + remote_connection: ssh_connection, delegate, multiplex_task, heartbeat_task, @@ -593,7 +585,7 @@ impl RemoteClient { }; State::Connected { - ssh_connection, + remote_connection: ssh_connection, delegate, multiplex_task, heartbeat_task: Self::heartbeat(this.clone(), connection_activity_rx, cx), @@ -866,6 +858,17 @@ impl RemoteClient { self.connection_options.clone() } + pub fn connection(&self) -> Option> { + if let State::Connected { + remote_connection, .. + } = self.state.as_ref()? + { + Some(remote_connection.clone()) + } else { + None + } + } + pub fn connection_state(&self) -> ConnectionState { self.state .as_ref() @@ -947,11 +950,15 @@ impl RemoteClient { client_cx: &mut gpui::TestAppContext, ) -> Entity { let (_tx, rx) = oneshot::channel(); + let mut cx = client_cx.to_async(); + let connection = connect(opts, Arc::new(fake::Delegate), &mut cx) + .await + .unwrap(); client_cx .update(|cx| { Self::new( ConnectionIdentifier::setup(), - opts, + connection, rx, Arc::new(fake::Delegate), cx, @@ -1084,7 +1091,7 @@ impl From for RemoteConnectionOptions { } #[async_trait(?Send)] -pub(crate) trait RemoteConnection: Send + Sync { +pub trait RemoteConnection: Send + Sync { fn start_proxy( &self, unique_identifier: String, diff --git a/crates/remote/src/transport.rs b/crates/remote/src/transport.rs index 33d98f4d6434b29b72535a23a9ce68a8eb85185c..8dddab2a5d1437240a1ee6a56052f8459f6921a1 100644 --- a/crates/remote/src/transport.rs +++ b/crates/remote/src/transport.rs @@ -183,6 +183,7 @@ async fn build_remote_server_from_source( log::info!("building remote server binary from source"); run_cmd( Command::new("cargo") + .current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../..")) .args([ "build", "--package", diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e6e087a98e803df16f240486d7650eab6ec62c61..e7942a3d86e33fcab4cb1ecb0a9c11d248fd90f9 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -76,7 +76,10 @@ use project::{ debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, toolchain_store::ToolchainStoreEvent, }; -use remote::{RemoteClientDelegate, RemoteConnectionOptions, remote_client::ConnectionIdentifier}; +use remote::{ + RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions, + remote_client::ConnectionIdentifier, +}; use schemars::JsonSchema; use serde::Deserialize; use session::AppSession; @@ -7441,22 +7444,23 @@ pub fn create_and_open_local_file( pub fn open_remote_project_with_new_connection( window: WindowHandle, - connection_options: RemoteConnectionOptions, + remote_connection: Arc, cancel_rx: oneshot::Receiver<()>, delegate: Arc, app_state: Arc, paths: Vec, cx: &mut App, -) -> Task> { +) -> Task>>>> { cx.spawn(async move |cx| { let (workspace_id, serialized_workspace) = - serialize_remote_project(connection_options.clone(), paths.clone(), cx).await?; + serialize_remote_project(remote_connection.connection_options(), paths.clone(), cx) + .await?; let session = match cx .update(|cx| { remote::RemoteClient::new( ConnectionIdentifier::Workspace(workspace_id.0), - connection_options, + remote_connection, cancel_rx, delegate, cx, @@ -7465,7 +7469,7 @@ pub fn open_remote_project_with_new_connection( .await? { Some(result) => result, - None => return Ok(()), + None => return Ok(Vec::new()), }; let project = cx.update(|cx| { @@ -7500,7 +7504,7 @@ pub fn open_remote_project_with_existing_connection( app_state: Arc, window: WindowHandle, cx: &mut AsyncApp, -) -> Task> { +) -> Task>>>> { cx.spawn(async move |cx| { let (workspace_id, serialized_workspace) = serialize_remote_project(connection_options.clone(), paths.clone(), cx).await?; @@ -7526,7 +7530,7 @@ async fn open_remote_project_inner( app_state: Arc, window: WindowHandle, cx: &mut AsyncApp, -) -> Result<()> { +) -> Result>>> { let toolchains = DB.toolchains(workspace_id).await?; for (toolchain, worktree_id, path) in toolchains { project @@ -7583,7 +7587,7 @@ async fn open_remote_project_inner( }); })?; - window + let items = window .update(cx, |_, window, cx| { window.activate_window(); open_items(serialized_workspace, project_paths_to_open, window, cx) @@ -7602,7 +7606,7 @@ async fn open_remote_project_inner( } })?; - Ok(()) + Ok(items.into_iter().map(|item| item?.ok()).collect()) } fn serialize_remote_project(