Add initial support for WSL (#37035)
Closes #36188 ## Todo * [x] CLI * [x] terminals * [x] tasks ## For future PRs * debugging * UI for opening WSL projects * fixing workspace state restoration Release Notes: - Windows alpha: Zed now supports editing folders in WSL. --------- Co-authored-by: Junkui Zhang <364772080@qq.com>
This commit is contained in:
co-authored by
Junkui Zhang
parent
1c2e2a00fe
commit
f78f3e7729
+42
-33
@@ -23,13 +23,14 @@ use http_client::{Url, read_proxy_from_env};
|
||||
use language::LanguageRegistry;
|
||||
use onboarding::{FIRST_OPEN, show_onboarding_view};
|
||||
use prompt_store::PromptBuilder;
|
||||
use remote::RemoteConnectionOptions;
|
||||
use reqwest_client::ReqwestClient;
|
||||
|
||||
use assets::Assets;
|
||||
use node_runtime::{NodeBinaryOptions, NodeRuntime};
|
||||
use parking_lot::Mutex;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use recent_projects::{SshSettings, open_ssh_project};
|
||||
use recent_projects::{SshSettings, open_remote_project};
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use session::{AppSession, Session};
|
||||
use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
|
||||
@@ -360,6 +361,7 @@ pub fn main() {
|
||||
open_listener.open(RawOpenRequest {
|
||||
urls,
|
||||
diff_paths: Vec::new(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
});
|
||||
@@ -696,7 +698,7 @@ pub fn main() {
|
||||
let urls: Vec<_> = args
|
||||
.paths_or_urls
|
||||
.iter()
|
||||
.filter_map(|arg| parse_url_arg(arg, cx).log_err())
|
||||
.map(|arg| parse_url_arg(arg, cx))
|
||||
.collect();
|
||||
|
||||
let diff_paths: Vec<[String; 2]> = args
|
||||
@@ -706,7 +708,11 @@ pub fn main() {
|
||||
.collect();
|
||||
|
||||
if !urls.is_empty() || !diff_paths.is_empty() {
|
||||
open_listener.open(RawOpenRequest { urls, diff_paths })
|
||||
open_listener.open(RawOpenRequest {
|
||||
urls,
|
||||
diff_paths,
|
||||
wsl: args.wsl,
|
||||
})
|
||||
}
|
||||
|
||||
match open_rx
|
||||
@@ -792,10 +798,10 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(connection_options) = request.ssh_connection {
|
||||
if let Some(connection_options) = request.remote_connection {
|
||||
cx.spawn(async move |cx| {
|
||||
let paths: Vec<PathBuf> = request.open_paths.into_iter().map(PathBuf::from).collect();
|
||||
open_ssh_project(
|
||||
open_remote_project(
|
||||
connection_options,
|
||||
paths,
|
||||
app_state,
|
||||
@@ -978,31 +984,24 @@ async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: &mut AsyncApp
|
||||
tasks.push(task);
|
||||
}
|
||||
}
|
||||
SerializedWorkspaceLocation::Ssh(ssh) => {
|
||||
SerializedWorkspaceLocation::Remote(mut connection_options) => {
|
||||
let app_state = app_state.clone();
|
||||
let ssh_host = ssh.host.clone();
|
||||
let task = cx.spawn(async move |cx| {
|
||||
let connection_options = cx.update(|cx| {
|
||||
if let RemoteConnectionOptions::Ssh(options) = &mut connection_options {
|
||||
cx.update(|cx| {
|
||||
SshSettings::get_global(cx)
|
||||
.connection_options_for(ssh.host, ssh.port, ssh.user)
|
||||
});
|
||||
|
||||
match connection_options {
|
||||
Ok(connection_options) => recent_projects::open_ssh_project(
|
||||
connection_options,
|
||||
paths.paths().into_iter().map(PathBuf::from).collect(),
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e)),
|
||||
Err(e) => Err(anyhow::anyhow!(
|
||||
"Failed to get SSH connection options for {}: {}",
|
||||
ssh_host,
|
||||
e
|
||||
)),
|
||||
}
|
||||
.fill_connection_options_from_settings(options)
|
||||
})?;
|
||||
}
|
||||
let task = cx.spawn(async move |cx| {
|
||||
recent_projects::open_remote_project(
|
||||
connection_options,
|
||||
paths.paths().into_iter().map(PathBuf::from).collect(),
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e))
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
@@ -1184,6 +1183,16 @@ struct Args {
|
||||
#[arg(long, value_name = "DIR")]
|
||||
user_data_dir: Option<String>,
|
||||
|
||||
/// The username and WSL distribution to use when opening paths. ,If not specified,
|
||||
/// Zed will attempt to open the paths directly.
|
||||
///
|
||||
/// The username is optional, and if not specified, the default user for the distribution
|
||||
/// will be used.
|
||||
///
|
||||
/// Example: `me@Ubuntu` or `Ubuntu` for default distribution.
|
||||
#[arg(long, value_name = "USER@DISTRO")]
|
||||
wsl: Option<String>,
|
||||
|
||||
/// Instructs zed to run as a dev server on this machine. (not implemented)
|
||||
#[arg(long)]
|
||||
dev_server_token: Option<String>,
|
||||
@@ -1242,18 +1251,18 @@ impl ToString for IdType {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_url_arg(arg: &str, cx: &App) -> Result<String> {
|
||||
fn parse_url_arg(arg: &str, cx: &App) -> String {
|
||||
match std::fs::canonicalize(Path::new(&arg)) {
|
||||
Ok(path) => Ok(format!("file://{}", path.display())),
|
||||
Err(error) => {
|
||||
Ok(path) => format!("file://{}", path.display()),
|
||||
Err(_) => {
|
||||
if arg.starts_with("file://")
|
||||
|| arg.starts_with("zed-cli://")
|
||||
|| arg.starts_with("ssh://")
|
||||
|| parse_zed_link(arg, cx).is_some()
|
||||
{
|
||||
Ok(arg.into())
|
||||
arg.into()
|
||||
} else {
|
||||
anyhow::bail!("error parsing path argument: {error}")
|
||||
format!("file://{arg}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ use project::{DirectoryLister, ProjectItem};
|
||||
use project_panel::ProjectPanel;
|
||||
use prompt_store::PromptBuilder;
|
||||
use quick_action_bar::QuickActionBar;
|
||||
use recent_projects::open_ssh_project;
|
||||
use recent_projects::open_remote_project;
|
||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||
use rope::Rope;
|
||||
use search::project_search::ProjectSearchBar;
|
||||
@@ -1557,7 +1557,7 @@ pub fn open_new_ssh_project_from_project(
|
||||
};
|
||||
let connection_options = ssh_client.read(cx).connection_options();
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
open_ssh_project(
|
||||
open_remote_project(
|
||||
connection_options,
|
||||
paths,
|
||||
app_state,
|
||||
|
||||
@@ -17,8 +17,8 @@ use gpui::{App, AsyncApp, Global, WindowHandle};
|
||||
use language::Point;
|
||||
use onboarding::FIRST_OPEN;
|
||||
use onboarding::show_onboarding_view;
|
||||
use recent_projects::{SshSettings, open_ssh_project};
|
||||
use remote::SshConnectionOptions;
|
||||
use recent_projects::{SshSettings, open_remote_project};
|
||||
use remote::{RemoteConnectionOptions, WslConnectionOptions};
|
||||
use settings::Settings;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
@@ -37,7 +37,7 @@ pub struct OpenRequest {
|
||||
pub diff_paths: Vec<[String; 2]>,
|
||||
pub open_channel_notes: Vec<(u64, Option<String>)>,
|
||||
pub join_channel: Option<u64>,
|
||||
pub ssh_connection: Option<SshConnectionOptions>,
|
||||
pub remote_connection: Option<RemoteConnectionOptions>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -51,6 +51,23 @@ pub enum OpenRequestKind {
|
||||
impl OpenRequest {
|
||||
pub fn parse(request: RawOpenRequest, cx: &App) -> Result<Self> {
|
||||
let mut this = Self::default();
|
||||
|
||||
this.diff_paths = request.diff_paths;
|
||||
if let Some(wsl) = request.wsl {
|
||||
let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
|
||||
if user.is_empty() {
|
||||
anyhow::bail!("user is empty in wsl argument");
|
||||
}
|
||||
(Some(user.to_string()), distro.to_string())
|
||||
} else {
|
||||
(None, wsl)
|
||||
};
|
||||
this.remote_connection = Some(RemoteConnectionOptions::Wsl(WslConnectionOptions {
|
||||
distro_name,
|
||||
user,
|
||||
}));
|
||||
}
|
||||
|
||||
for url in request.urls {
|
||||
if let Some(server_name) = url.strip_prefix("zed-cli://") {
|
||||
this.kind = Some(OpenRequestKind::CliConnection(connect_to_cli(server_name)?));
|
||||
@@ -80,8 +97,6 @@ impl OpenRequest {
|
||||
}
|
||||
}
|
||||
|
||||
this.diff_paths = request.diff_paths;
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
@@ -108,13 +123,15 @@ impl OpenRequest {
|
||||
if let Some(password) = url.password() {
|
||||
connection_options.password = Some(password.to_string());
|
||||
}
|
||||
if let Some(ssh_connection) = &self.ssh_connection {
|
||||
|
||||
let connection_options = RemoteConnectionOptions::Ssh(connection_options);
|
||||
if let Some(ssh_connection) = &self.remote_connection {
|
||||
anyhow::ensure!(
|
||||
*ssh_connection == connection_options,
|
||||
"cannot open multiple ssh connections"
|
||||
"cannot open multiple different remote connections"
|
||||
);
|
||||
}
|
||||
self.ssh_connection = Some(connection_options);
|
||||
self.remote_connection = Some(connection_options);
|
||||
self.parse_file_path(url.path());
|
||||
Ok(())
|
||||
}
|
||||
@@ -152,6 +169,7 @@ pub struct OpenListener(UnboundedSender<RawOpenRequest>);
|
||||
pub struct RawOpenRequest {
|
||||
pub urls: Vec<String>,
|
||||
pub diff_paths: Vec<[String; 2]>,
|
||||
pub wsl: Option<String>,
|
||||
}
|
||||
|
||||
impl Global for OpenListener {}
|
||||
@@ -303,13 +321,21 @@ pub async fn handle_cli_connection(
|
||||
paths,
|
||||
diff_paths,
|
||||
wait,
|
||||
wsl,
|
||||
open_new_workspace,
|
||||
env,
|
||||
user_data_dir: _,
|
||||
} => {
|
||||
if !urls.is_empty() {
|
||||
cx.update(|cx| {
|
||||
match OpenRequest::parse(RawOpenRequest { urls, diff_paths }, cx) {
|
||||
match OpenRequest::parse(
|
||||
RawOpenRequest {
|
||||
urls,
|
||||
diff_paths,
|
||||
wsl,
|
||||
},
|
||||
cx,
|
||||
) {
|
||||
Ok(open_request) => {
|
||||
handle_open_request(open_request, app_state.clone(), cx);
|
||||
responses.send(CliResponse::Exit { status: 0 }).log_err();
|
||||
@@ -422,30 +448,26 @@ async fn open_workspaces(
|
||||
errored = true
|
||||
}
|
||||
}
|
||||
SerializedWorkspaceLocation::Ssh(ssh) => {
|
||||
SerializedWorkspaceLocation::Remote(mut connection) => {
|
||||
let app_state = app_state.clone();
|
||||
let connection_options = cx.update(|cx| {
|
||||
SshSettings::get_global(cx)
|
||||
.connection_options_for(ssh.host, ssh.port, ssh.user)
|
||||
});
|
||||
if let Ok(connection_options) = connection_options {
|
||||
cx.spawn(async move |cx| {
|
||||
open_ssh_project(
|
||||
connection_options,
|
||||
workspace_paths.paths().to_vec(),
|
||||
app_state,
|
||||
OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
// We don't set `errored` here if `open_ssh_project` fails, because for ssh projects, the
|
||||
// error is displayed in the window.
|
||||
} else {
|
||||
errored = false;
|
||||
if let RemoteConnectionOptions::Ssh(options) = &mut connection {
|
||||
cx.update(|cx| {
|
||||
SshSettings::get_global(cx)
|
||||
.fill_connection_options_from_settings(options)
|
||||
})?;
|
||||
}
|
||||
cx.spawn(async move |cx| {
|
||||
open_remote_project(
|
||||
connection,
|
||||
workspace_paths.paths().to_vec(),
|
||||
app_state,
|
||||
OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -587,6 +609,7 @@ mod tests {
|
||||
};
|
||||
use editor::Editor;
|
||||
use gpui::TestAppContext;
|
||||
use remote::SshConnectionOptions;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use util::path;
|
||||
@@ -609,8 +632,8 @@ mod tests {
|
||||
.unwrap()
|
||||
});
|
||||
assert_eq!(
|
||||
request.ssh_connection.unwrap(),
|
||||
SshConnectionOptions {
|
||||
request.remote_connection.unwrap(),
|
||||
RemoteConnectionOptions::Ssh(SshConnectionOptions {
|
||||
host: "localhost".into(),
|
||||
username: Some("me".into()),
|
||||
port: None,
|
||||
@@ -619,7 +642,7 @@ mod tests {
|
||||
port_forwards: None,
|
||||
nickname: None,
|
||||
upload_binary_over_ssh: false,
|
||||
}
|
||||
})
|
||||
);
|
||||
assert_eq!(request.open_paths, vec!["/"]);
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> {
|
||||
urls,
|
||||
diff_paths,
|
||||
wait: false,
|
||||
wsl: args.wsl.clone(),
|
||||
open_new_workspace: None,
|
||||
env: None,
|
||||
user_data_dir: args.user_data_dir.clone(),
|
||||
|
||||
Reference in New Issue
Block a user