Detailed changes
@@ -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
@@ -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
+```
@@ -155,6 +155,7 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
}
fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
+ 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<String> {
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<()> {
@@ -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<RemoteConnectionPrompt>,
- window: &mut Window,
- cx: &mut App,
-) -> Task<Result<Option<Entity<RemoteClient>>>> {
- 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::<Editor>() {
+ 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<dyn RemoteConnection>,
+ mut paths: Vec<PathBuf>,
+) -> (Vec<PathBuf>, Vec<PathWithPosition>) {
+ let mut paths_with_positions = Vec::<PathWithPosition>::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<dyn RemoteConnection>, 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())
+}
@@ -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::<Editor>() {
+ 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,
@@ -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;
@@ -93,7 +93,7 @@ const MAX_RECONNECT_ATTEMPTS: usize = 3;
enum State {
Connecting,
Connected {
- ssh_connection: Arc<dyn RemoteConnection>,
+ remote_connection: Arc<dyn RemoteConnection>,
delegate: Arc<dyn RemoteClientDelegate>,
multiplex_task: Task<Result<()>>,
@@ -137,7 +137,10 @@ impl fmt::Display for State {
impl State {
fn remote_connection(&self) -> Option<Arc<dyn RemoteConnection>> {
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<RemoteClientEvent> 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<dyn RemoteClientDelegate>,
- cx: &mut App,
- ) -> Task<Result<Option<Entity<Self>>>> {
- Self::new(
- unique_identifier,
- RemoteConnectionOptions::Ssh(connection_options),
- cancellation,
- delegate,
- cx,
- )
- }
+pub async fn connect(
+ connection_options: RemoteConnectionOptions,
+ delegate: Arc<dyn RemoteClientDelegate>,
+ cx: &mut AsyncApp,
+) -> Result<Arc<dyn RemoteConnection>> {
+ 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<dyn RemoteConnection>,
cancellation: oneshot::Receiver<()>,
delegate: Arc<dyn RemoteClientDelegate>,
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<Arc<dyn RemoteConnection>> {
+ 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<Self> {
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<WslConnectionOptions> for RemoteConnectionOptions {
}
#[async_trait(?Send)]
-pub(crate) trait RemoteConnection: Send + Sync {
+pub trait RemoteConnection: Send + Sync {
fn start_proxy(
&self,
unique_identifier: String,
@@ -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",
@@ -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<Workspace>,
- connection_options: RemoteConnectionOptions,
+ remote_connection: Arc<dyn RemoteConnection>,
cancel_rx: oneshot::Receiver<()>,
delegate: Arc<dyn RemoteClientDelegate>,
app_state: Arc<AppState>,
paths: Vec<PathBuf>,
cx: &mut App,
-) -> Task<Result<()>> {
+) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
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<AppState>,
window: WindowHandle<Workspace>,
cx: &mut AsyncApp,
-) -> Task<Result<()>> {
+) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
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<AppState>,
window: WindowHandle<Workspace>,
cx: &mut AsyncApp,
-) -> Result<()> {
+) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
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(