Add WSL opening UI (#38260)

localcc created

This PR adds an option to open WSL machines from the UI.

- [x] Open wsl from open remote
- [ ] Open local folder in wsl action
- [ ] Open wsl shortcut (shortcuts to open remote)

Release Notes:

- N/A

Change summary

Cargo.lock                                       |  54 +
crates/recent_projects/Cargo.toml                |   3 
crates/recent_projects/src/remote_connections.rs |  83 +
crates/recent_projects/src/remote_servers.rs     | 778 ++++++++++++++---
4 files changed, 744 insertions(+), 174 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2977,7 +2977,7 @@ dependencies = [
  "num-traits",
  "serde",
  "wasm-bindgen",
- "windows-link",
+ "windows-link 0.1.1",
 ]
 
 [[package]]
@@ -13790,6 +13790,7 @@ dependencies = [
  "theme",
  "ui",
  "util",
+ "windows-registry 0.6.0",
  "workspace",
  "workspace-hack",
  "zed_actions",
@@ -19613,7 +19614,7 @@ dependencies = [
  "windows-collections",
  "windows-core 0.61.0",
  "windows-future",
- "windows-link",
+ "windows-link 0.1.1",
  "windows-numerics",
 ]
 
@@ -19683,7 +19684,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
 dependencies = [
  "windows-implement 0.60.0",
  "windows-interface 0.59.1",
- "windows-link",
+ "windows-link 0.1.1",
  "windows-result 0.3.2",
  "windows-strings 0.4.0",
 ]
@@ -19695,7 +19696,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
 dependencies = [
  "windows-core 0.61.0",
- "windows-link",
+ "windows-link 0.1.1",
 ]
 
 [[package]]
@@ -19770,6 +19771,12 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
 
+[[package]]
+name = "windows-link"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
+
 [[package]]
 name = "windows-numerics"
 version = "0.2.0"
@@ -19777,7 +19784,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
 dependencies = [
  "windows-core 0.61.0",
- "windows-link",
+ "windows-link 0.1.1",
 ]
 
 [[package]]
@@ -19797,11 +19804,22 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e"
 dependencies = [
- "windows-link",
+ "windows-link 0.1.1",
  "windows-result 0.3.2",
  "windows-strings 0.4.0",
 ]
 
+[[package]]
+name = "windows-registry"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f91f87ce112ffb7275000ea98eb1940912c21c1567c9312fde20261f3eadd29"
+dependencies = [
+ "windows-link 0.2.0",
+ "windows-result 0.4.0",
+ "windows-strings 0.5.0",
+]
+
 [[package]]
 name = "windows-result"
 version = "0.1.2"
@@ -19826,7 +19844,16 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
 dependencies = [
- "windows-link",
+ "windows-link 0.1.1",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
+dependencies = [
+ "windows-link 0.2.0",
 ]
 
 [[package]]
@@ -19845,7 +19872,7 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
 dependencies = [
- "windows-link",
+ "windows-link 0.1.1",
 ]
 
 [[package]]
@@ -19854,7 +19881,16 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
 dependencies = [
- "windows-link",
+ "windows-link 0.1.1",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
+dependencies = [
+ "windows-link 0.2.0",
 ]
 
 [[package]]

crates/recent_projects/Cargo.toml 🔗

@@ -44,6 +44,9 @@ workspace.workspace = true
 zed_actions.workspace = true
 workspace-hack.workspace = true
 
+[target.'cfg(target_os = "windows")'.dependencies]
+windows-registry = "0.6.0"
+
 [dev-dependencies]
 dap.workspace = true
 editor = { workspace = true, features = ["test-support"] }

crates/recent_projects/src/remote_connections.rs 🔗

@@ -17,7 +17,7 @@ use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 use release_channel::ReleaseChannel;
 use remote::{
     ConnectionIdentifier, RemoteClient, RemoteConnectionOptions, RemotePlatform,
-    SshConnectionOptions, SshPortForwardOption,
+    SshConnectionOptions, SshPortForwardOption, WslConnectionOptions,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -33,6 +33,7 @@ use workspace::{AppState, ModalView, Workspace};
 #[derive(Deserialize)]
 pub struct SshSettings {
     pub ssh_connections: Option<Vec<SshConnection>>,
+    pub wsl_connections: Option<Vec<WslConnection>>,
     /// Whether to read ~/.ssh/config for ssh connection sources.
     #[serde(default = "default_true")]
     pub read_ssh_config: bool,
@@ -43,6 +44,10 @@ impl SshSettings {
         self.ssh_connections.clone().into_iter().flatten()
     }
 
+    pub fn wsl_connections(&self) -> impl Iterator<Item = WslConnection> + use<> {
+        self.wsl_connections.clone().into_iter().flatten()
+    }
+
     pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) {
         for conn in self.ssh_connections() {
             if conn.host == options.host
@@ -116,6 +121,51 @@ impl From<SshConnection> for SshConnectionOptions {
     }
 }
 
+#[derive(Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct WslConnection {
+    pub distro_name: SharedString,
+    #[serde(default)]
+    pub user: Option<String>,
+    #[serde(default)]
+    pub projects: BTreeSet<SshProject>,
+}
+
+impl From<WslConnection> for WslConnectionOptions {
+    fn from(val: WslConnection) -> Self {
+        WslConnectionOptions {
+            distro_name: val.distro_name.into(),
+            user: val.user,
+        }
+    }
+}
+
+#[derive(Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub enum Connection {
+    Ssh(SshConnection),
+    Wsl(WslConnection),
+}
+
+impl From<Connection> for RemoteConnectionOptions {
+    fn from(val: Connection) -> Self {
+        match val {
+            Connection::Ssh(conn) => RemoteConnectionOptions::Ssh(conn.into()),
+            Connection::Wsl(conn) => RemoteConnectionOptions::Wsl(conn.into()),
+        }
+    }
+}
+
+impl From<SshConnection> for Connection {
+    fn from(val: SshConnection) -> Self {
+        Connection::Ssh(val)
+    }
+}
+
+impl From<WslConnection> for Connection {
+    fn from(val: WslConnection) -> Self {
+        Connection::Wsl(val)
+    }
+}
+
 #[derive(Clone, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema)]
 pub struct SshProject {
     pub paths: Vec<String>,
@@ -125,6 +175,7 @@ pub struct SshProject {
 #[settings_key(None)]
 pub struct RemoteSettingsContent {
     pub ssh_connections: Option<Vec<SshConnection>>,
+    pub wsl_connections: Option<Vec<WslConnection>>,
     pub read_ssh_config: Option<bool>,
 }
 
@@ -561,6 +612,36 @@ pub fn connect_over_ssh(
     )
 }
 
+pub fn connect(
+    unique_identifier: ConnectionIdentifier,
+    connection_options: RemoteConnectionOptions,
+    ui: Entity<RemoteConnectionPrompt>,
+    window: &mut Window,
+    cx: &mut App,
+) -> Task<Result<Option<Entity<RemoteClient>>>> {
+    let window = window.window_handle();
+    let known_password = match &connection_options {
+        RemoteConnectionOptions::Ssh(ssh_connection_options) => {
+            ssh_connection_options.password.clone()
+        }
+        _ => None,
+    };
+    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,
+    )
+}
+
 pub async fn open_remote_project(
     connection_options: RemoteConnectionOptions,
     paths: Vec<PathBuf>,

crates/recent_projects/src/remote_servers.rs 🔗

@@ -1,7 +1,8 @@
 use crate::{
     remote_connections::{
-        RemoteConnectionModal, RemoteConnectionPrompt, RemoteSettingsContent, SshConnection,
-        SshConnectionHeader, SshProject, SshSettings, connect_over_ssh, open_remote_project,
+        Connection, RemoteConnectionModal, RemoteConnectionPrompt, RemoteSettingsContent,
+        SshConnection, SshConnectionHeader, SshProject, SshSettings, connect, connect_over_ssh,
+        open_remote_project,
     },
     ssh_config::parse_ssh_config_hosts,
 };
@@ -13,11 +14,12 @@ use gpui::{
     FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task, WeakEntity, Window,
     canvas,
 };
+use log::info;
 use paths::{global_ssh_config_file, user_ssh_config_file};
 use picker::Picker;
 use project::{Fs, Project};
 use remote::{
-    RemoteClient, RemoteConnectionOptions, SshConnectionOptions,
+    RemoteClient, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions,
     remote_client::ConnectionIdentifier,
 };
 use settings::{Settings, SettingsStore, update_settings_file, watch_config_file};
@@ -79,27 +81,253 @@ impl CreateRemoteServer {
     }
 }
 
+#[cfg(target_os = "windows")]
+struct AddWslDistro {
+    picker: Entity<Picker<WslPickerDelegate>>,
+    connection_prompt: Option<Entity<RemoteConnectionPrompt>>,
+    _creating: Option<Task<()>>,
+}
+
+#[cfg(target_os = "windows")]
+impl AddWslDistro {
+    fn new(window: &mut Window, cx: &mut Context<RemoteServerProjects>) -> Self {
+        let delegate = WslPickerDelegate::new();
+        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
+
+        cx.subscribe_in(
+            &picker,
+            window,
+            |this, _, _: &WslDistroSelected, window, cx| {
+                this.confirm(&menu::Confirm, window, cx);
+            },
+        )
+        .detach();
+
+        cx.subscribe_in(
+            &picker,
+            window,
+            |this, _, _: &WslPickerDismissed, window, cx| {
+                this.cancel(&menu::Cancel, window, cx);
+            },
+        )
+        .detach();
+
+        AddWslDistro {
+            picker,
+            connection_prompt: None,
+            _creating: None,
+        }
+    }
+}
+
+#[cfg(target_os = "windows")]
+#[derive(Clone, Debug)]
+pub struct WslDistroSelected(pub String);
+
+#[cfg(target_os = "windows")]
+#[derive(Clone, Debug)]
+pub struct WslPickerDismissed;
+
+#[cfg(target_os = "windows")]
+struct WslPickerDelegate {
+    selected_index: usize,
+    distro_list: Option<Vec<String>>,
+    matches: Vec<fuzzy::StringMatch>,
+}
+
+#[cfg(target_os = "windows")]
+impl WslPickerDelegate {
+    fn new() -> Self {
+        WslPickerDelegate {
+            selected_index: 0,
+            distro_list: None,
+            matches: Vec::new(),
+        }
+    }
+
+    pub fn selected_distro(&self) -> Option<String> {
+        self.matches
+            .get(self.selected_index)
+            .map(|m| m.string.clone())
+    }
+}
+
+#[cfg(target_os = "windows")]
+impl WslPickerDelegate {
+    fn fetch_distros() -> anyhow::Result<Vec<String>> {
+        use anyhow::Context;
+        use windows_registry::CURRENT_USER;
+
+        let lxss_key = CURRENT_USER
+            .open("Software\\Microsoft\\Windows\\CurrentVersion\\Lxss")
+            .context("failed to get lxss wsl key")?;
+
+        let distros = lxss_key
+            .keys()
+            .context("failed to get wsl distros")?
+            .filter_map(|key| {
+                lxss_key
+                    .open(&key)
+                    .context("failed to open subkey for distro")
+                    .log_err()
+            })
+            .filter_map(|distro| distro.get_string("DistributionName").ok())
+            .collect::<Vec<_>>();
+
+        Ok(distros)
+    }
+}
+
+#[cfg(target_os = "windows")]
+impl EventEmitter<WslDistroSelected> for Picker<WslPickerDelegate> {}
+
+#[cfg(target_os = "windows")]
+impl EventEmitter<WslPickerDismissed> for Picker<WslPickerDelegate> {}
+
+#[cfg(target_os = "windows")]
+impl picker::PickerDelegate for WslPickerDelegate {
+    type ListItem = ListItem;
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(
+        &mut self,
+        ix: usize,
+        _window: &mut Window,
+        cx: &mut Context<Picker<Self>>,
+    ) {
+        self.selected_index = ix;
+        cx.notify();
+    }
+
+    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+        Arc::from("Enter WSL distro name")
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        _window: &mut Window,
+        cx: &mut Context<Picker<Self>>,
+    ) -> Task<()> {
+        use fuzzy::StringMatchCandidate;
+
+        let needs_fetch = self.distro_list.is_none();
+        if needs_fetch {
+            let distros = Self::fetch_distros().log_err();
+            self.distro_list = distros;
+        }
+
+        if let Some(distro_list) = &self.distro_list {
+            use ordered_float::OrderedFloat;
+
+            let candidates = distro_list
+                .iter()
+                .enumerate()
+                .map(|(id, distro)| StringMatchCandidate::new(id, distro))
+                .collect::<Vec<_>>();
+
+            let query = query.trim_start();
+            let smart_case = query.chars().any(|c| c.is_uppercase());
+            self.matches = smol::block_on(fuzzy::match_strings(
+                candidates.as_slice(),
+                query,
+                smart_case,
+                true,
+                100,
+                &Default::default(),
+                cx.background_executor().clone(),
+            ));
+            self.matches.sort_unstable_by_key(|m| m.candidate_id);
+
+            self.selected_index = self
+                .matches
+                .iter()
+                .enumerate()
+                .rev()
+                .max_by_key(|(_, m)| OrderedFloat(m.score))
+                .map(|(index, _)| index)
+                .unwrap_or(0);
+        }
+
+        Task::ready(())
+    }
+
+    fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        if let Some(distro) = self.matches.get(self.selected_index) {
+            cx.emit(WslDistroSelected(distro.string.clone()));
+        }
+    }
+
+    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        cx.emit(WslPickerDismissed);
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        _: &mut Window,
+        _: &mut Context<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        use ui::HighlightedLabel;
+
+        let matched = self.matches.get(ix)?;
+        Some(
+            ListItem::new(ix)
+                .toggle_state(selected)
+                .inset(true)
+                .spacing(ui::ListItemSpacing::Sparse)
+                .child(
+                    h_flex()
+                        .flex_grow()
+                        .gap_3()
+                        .child(Icon::new(IconName::Server))
+                        .child(v_flex().child(HighlightedLabel::new(
+                            matched.string.clone(),
+                            matched.positions.clone(),
+                        ))),
+                ),
+        )
+    }
+}
+
+enum ProjectPickerData {
+    Ssh {
+        connection_string: SharedString,
+        nickname: Option<SharedString>,
+    },
+    Wsl {
+        distro_name: SharedString,
+    },
+}
+
 struct ProjectPicker {
-    connection_string: SharedString,
-    nickname: Option<SharedString>,
+    data: ProjectPickerData,
     picker: Entity<Picker<OpenPathDelegate>>,
     _path_task: Shared<Task<Option<()>>>,
 }
 
 struct EditNicknameState {
-    index: usize,
+    index: SshServerIndex,
     editor: Entity<Editor>,
 }
 
 impl EditNicknameState {
-    fn new(index: usize, window: &mut Window, cx: &mut App) -> Self {
+    fn new(index: SshServerIndex, window: &mut Window, cx: &mut App) -> Self {
         let this = Self {
             index,
             editor: cx.new(|cx| Editor::single_line(window, cx)),
         };
         let starting_text = SshSettings::get_global(cx)
             .ssh_connections()
-            .nth(index)
+            .nth(index.0)
             .and_then(|state| state.nickname)
             .filter(|text| !text.is_empty());
         this.editor.update(cx, |this, cx| {
@@ -122,8 +350,8 @@ impl Focusable for ProjectPicker {
 impl ProjectPicker {
     fn new(
         create_new_window: bool,
-        ix: usize,
-        connection: SshConnectionOptions,
+        index: ServerIndex,
+        connection: RemoteConnectionOptions,
         project: Entity<Project>,
         home_dir: RemotePathBuf,
         path_style: PathStyle,
@@ -142,8 +370,16 @@ impl ProjectPicker {
             picker.set_query(home_dir.to_string(), window, cx);
             picker
         });
-        let connection_string = connection.connection_string().into();
-        let nickname = connection.nickname.clone().map(|nick| nick.into());
+
+        let data = match &connection {
+            RemoteConnectionOptions::Ssh(connection) => ProjectPickerData::Ssh {
+                connection_string: connection.connection_string().into(),
+                nickname: connection.nickname.clone().map(|nick| nick.into()),
+            },
+            RemoteConnectionOptions::Wsl(connection) => ProjectPickerData::Wsl {
+                distro_name: connection.distro_name.clone().into(),
+            },
+        };
         let _path_task = cx
             .spawn_in(window, {
                 let workspace = workspace;
@@ -178,13 +414,24 @@ impl ProjectPicker {
                                 .iter()
                                 .map(|path| path.to_string_lossy().to_string())
                                 .collect();
-                            move |setting, _| {
-                                if let Some(server) = setting
-                                    .ssh_connections
-                                    .as_mut()
-                                    .and_then(|connections| connections.get_mut(ix))
-                                {
-                                    server.projects.insert(SshProject { paths });
+                            move |setting, _| match index {
+                                ServerIndex::Ssh(index) => {
+                                    if let Some(server) = setting
+                                        .ssh_connections
+                                        .as_mut()
+                                        .and_then(|connections| connections.get_mut(index.0))
+                                    {
+                                        server.projects.insert(SshProject { paths });
+                                    };
+                                }
+                                ServerIndex::Wsl(index) => {
+                                    if let Some(server) = setting
+                                        .wsl_connections
+                                        .as_mut()
+                                        .and_then(|connections| connections.get_mut(index.0))
+                                    {
+                                        server.projects.insert(SshProject { paths });
+                                    };
                                 }
                             }
                         });
@@ -204,12 +451,7 @@ impl ProjectPicker {
                         .log_err()?;
 
                     open_remote_project_with_existing_connection(
-                        RemoteConnectionOptions::Ssh(connection),
-                        project,
-                        paths,
-                        app_state,
-                        window,
-                        cx,
+                        connection, project, paths, app_state, window, cx,
                     )
                     .await
                     .log_err();
@@ -225,8 +467,7 @@ impl ProjectPicker {
         cx.new(|_| Self {
             _path_task,
             picker,
-            connection_string,
-            nickname,
+            data,
         })
     }
 }
@@ -234,14 +475,23 @@ impl ProjectPicker {
 impl gpui::Render for ProjectPicker {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         v_flex()
-            .child(
-                SshConnectionHeader {
-                    connection_string: self.connection_string.clone(),
+            .child(match &self.data {
+                ProjectPickerData::Ssh {
+                    connection_string,
+                    nickname,
+                } => SshConnectionHeader {
+                    connection_string: connection_string.clone(),
                     paths: Default::default(),
-                    nickname: self.nickname.clone(),
+                    nickname: nickname.clone(),
                 }
                 .render(window, cx),
-            )
+                ProjectPickerData::Wsl { distro_name } => SshConnectionHeader {
+                    connection_string: distro_name.clone(),
+                    paths: Default::default(),
+                    nickname: None,
+                }
+                .render(window, cx),
+            })
             .child(
                 div()
                     .border_t_1()
@@ -251,13 +501,48 @@ impl gpui::Render for ProjectPicker {
     }
 }
 
+#[repr(transparent)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+struct SshServerIndex(usize);
+impl std::fmt::Display for SshServerIndex {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+#[repr(transparent)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+struct WslServerIndex(usize);
+impl std::fmt::Display for WslServerIndex {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+enum ServerIndex {
+    Ssh(SshServerIndex),
+    Wsl(WslServerIndex),
+}
+impl From<SshServerIndex> for ServerIndex {
+    fn from(index: SshServerIndex) -> Self {
+        Self::Ssh(index)
+    }
+}
+impl From<WslServerIndex> for ServerIndex {
+    fn from(index: WslServerIndex) -> Self {
+        Self::Wsl(index)
+    }
+}
+
 #[derive(Clone)]
 enum RemoteEntry {
     Project {
         open_folder: NavigableEntry,
         projects: Vec<(NavigableEntry, SshProject)>,
         configure: NavigableEntry,
-        connection: SshConnection,
+        connection: Connection,
+        index: ServerIndex,
     },
     SshConfig {
         open_folder: NavigableEntry,
@@ -270,13 +555,16 @@ impl RemoteEntry {
         matches!(self, Self::Project { .. })
     }
 
-    fn connection(&self) -> Cow<'_, SshConnection> {
+    fn connection(&self) -> Cow<'_, Connection> {
         match self {
             Self::Project { connection, .. } => Cow::Borrowed(connection),
-            Self::SshConfig { host, .. } => Cow::Owned(SshConnection {
-                host: host.clone(),
-                ..SshConnection::default()
-            }),
+            Self::SshConfig { host, .. } => Cow::Owned(
+                SshConnection {
+                    host: host.clone(),
+                    ..SshConnection::default()
+                }
+                .into(),
+            ),
         }
     }
 }
@@ -285,6 +573,7 @@ impl RemoteEntry {
 struct DefaultState {
     scroll_handle: ScrollHandle,
     add_new_server: NavigableEntry,
+    add_new_wsl: NavigableEntry,
     servers: Vec<RemoteEntry>,
 }
 
@@ -292,13 +581,15 @@ impl DefaultState {
     fn new(ssh_config_servers: &BTreeSet<SharedString>, cx: &mut App) -> Self {
         let handle = ScrollHandle::new();
         let add_new_server = NavigableEntry::new(&handle, cx);
+        let add_new_wsl = NavigableEntry::new(&handle, cx);
 
         let ssh_settings = SshSettings::get_global(cx);
         let read_ssh_config = ssh_settings.read_ssh_config;
 
-        let mut servers: Vec<RemoteEntry> = ssh_settings
+        let ssh_servers = ssh_settings
             .ssh_connections()
-            .map(|connection| {
+            .enumerate()
+            .map(|(index, connection)| {
                 let open_folder = NavigableEntry::new(&handle, cx);
                 let configure = NavigableEntry::new(&handle, cx);
                 let projects = connection
@@ -310,16 +601,42 @@ impl DefaultState {
                     open_folder,
                     configure,
                     projects,
-                    connection,
+                    index: ServerIndex::Ssh(SshServerIndex(index)),
+                    connection: connection.into(),
                 }
-            })
-            .collect();
+            });
+
+        let wsl_servers = ssh_settings
+            .wsl_connections()
+            .enumerate()
+            .map(|(index, connection)| {
+                let open_folder = NavigableEntry::new(&handle, cx);
+                let configure = NavigableEntry::new(&handle, cx);
+                let projects = connection
+                    .projects
+                    .iter()
+                    .map(|project| (NavigableEntry::new(&handle, cx), project.clone()))
+                    .collect();
+                RemoteEntry::Project {
+                    open_folder,
+                    configure,
+                    projects,
+                    index: ServerIndex::Wsl(WslServerIndex(index)),
+                    connection: connection.into(),
+                }
+            });
+
+        let mut servers = ssh_servers.chain(wsl_servers).collect::<Vec<RemoteEntry>>();
 
         if read_ssh_config {
             let mut extra_servers_from_config = ssh_config_servers.clone();
             for server in &servers {
-                if let RemoteEntry::Project { connection, .. } = server {
-                    extra_servers_from_config.remove(&connection.host);
+                if let RemoteEntry::Project {
+                    connection: Connection::Ssh(ssh_options),
+                    ..
+                } = server
+                {
+                    extra_servers_from_config.remove(&SharedString::new(ssh_options.host.clone()));
                 }
             }
             servers.extend(extra_servers_from_config.into_iter().map(|host| {
@@ -333,23 +650,43 @@ impl DefaultState {
         Self {
             scroll_handle: handle,
             add_new_server,
+            add_new_wsl,
             servers,
         }
     }
 }
 
 #[derive(Clone)]
-struct ViewServerOptionsState {
-    server_index: usize,
-    connection: SshConnection,
-    entries: [NavigableEntry; 4],
+enum ViewServerOptionsState {
+    Ssh {
+        connection: SshConnectionOptions,
+        server_index: SshServerIndex,
+        entries: [NavigableEntry; 4],
+    },
+    Wsl {
+        connection: WslConnectionOptions,
+        server_index: WslServerIndex,
+        entries: [NavigableEntry; 2],
+    },
 }
+
+impl ViewServerOptionsState {
+    fn entries(&self) -> &[NavigableEntry] {
+        match self {
+            Self::Ssh { entries, .. } => entries,
+            Self::Wsl { entries, .. } => entries,
+        }
+    }
+}
+
 enum Mode {
     Default(DefaultState),
     ViewServerOptions(ViewServerOptionsState),
     EditNickname(EditNicknameState),
     ProjectPicker(Entity<ProjectPicker>),
     CreateRemoteServer(CreateRemoteServer),
+    #[cfg(target_os = "windows")]
+    AddWslDistro(AddWslDistro),
 }
 
 impl Mode {
@@ -357,6 +694,7 @@ impl Mode {
         Self::Default(DefaultState::new(ssh_config_servers, cx))
     }
 }
+
 impl RemoteServerProjects {
     pub fn new(
         create_new_window: bool,
@@ -405,10 +743,10 @@ impl RemoteServerProjects {
         }
     }
 
-    pub fn project_picker(
+    fn project_picker(
         create_new_window: bool,
-        ix: usize,
-        connection_options: remote::SshConnectionOptions,
+        index: ServerIndex,
+        connection_options: remote::RemoteConnectionOptions,
         project: Entity<Project>,
         home_dir: RemotePathBuf,
         path_style: PathStyle,
@@ -420,7 +758,7 @@ impl RemoteServerProjects {
         let mut this = Self::new(create_new_window, fs, window, workspace.clone(), cx);
         this.mode = Mode::ProjectPicker(ProjectPicker::new(
             create_new_window,
-            ix,
+            index,
             connection_options,
             project,
             home_dir,
@@ -480,6 +818,7 @@ impl RemoteServerProjects {
             match connection.await {
                 Some(Some(client)) => this
                     .update_in(cx, |this, window, cx| {
+                        info!("ssh server created");
                         telemetry::event!("SSH Server Created");
                         this.retained_connections.push(client);
                         this.add_ssh_server(connection_options, cx);
@@ -517,25 +856,100 @@ impl RemoteServerProjects {
         });
     }
 
+    #[cfg(target_os = "windows")]
+    fn connect_wsl_distro(
+        &mut self,
+        picker: Entity<Picker<WslPickerDelegate>>,
+        distro: String,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let connection_options = WslConnectionOptions {
+            distro_name: distro,
+            user: None,
+        };
+
+        let prompt = cx.new(|cx| {
+            RemoteConnectionPrompt::new(connection_options.distro_name.clone(), None, window, cx)
+        });
+        let connection = connect(
+            ConnectionIdentifier::setup(),
+            connection_options.clone().into(),
+            prompt.clone(),
+            window,
+            cx,
+        )
+        .prompt_err("Failed to connect", window, cx, |_, _, _| None);
+
+        let wsl_picker = picker.clone();
+        let creating = cx.spawn_in(window, async move |this, cx| {
+            match connection.await {
+                Some(Some(client)) => this
+                    .update_in(cx, |this, window, cx| {
+                        telemetry::event!("WSL Distro Added");
+                        this.retained_connections.push(client);
+                        this.add_wsl_distro(connection_options, cx);
+                        this.mode = Mode::default_mode(&BTreeSet::new(), cx);
+                        this.focus_handle(cx).focus(window);
+                        cx.notify()
+                    })
+                    .log_err(),
+                _ => this
+                    .update(cx, |this, cx| {
+                        this.mode = Mode::AddWslDistro(AddWslDistro {
+                            picker: wsl_picker,
+                            connection_prompt: None,
+                            _creating: None,
+                        });
+                        cx.notify()
+                    })
+                    .log_err(),
+            };
+            ()
+        });
+
+        self.mode = Mode::AddWslDistro(AddWslDistro {
+            picker,
+            connection_prompt: Some(prompt),
+            _creating: Some(creating),
+        });
+    }
+
     fn view_server_options(
         &mut self,
-        (server_index, connection): (usize, SshConnection),
+        (server_index, connection): (ServerIndex, RemoteConnectionOptions),
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.mode = Mode::ViewServerOptions(ViewServerOptionsState {
-            server_index,
-            connection,
-            entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)),
+        self.mode = Mode::ViewServerOptions(match (server_index, connection) {
+            (ServerIndex::Ssh(server_index), RemoteConnectionOptions::Ssh(connection)) => {
+                ViewServerOptionsState::Ssh {
+                    connection,
+                    server_index,
+                    entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)),
+                }
+            }
+            (ServerIndex::Wsl(server_index), RemoteConnectionOptions::Wsl(connection)) => {
+                ViewServerOptionsState::Wsl {
+                    connection,
+                    server_index,
+                    entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)),
+                }
+            }
+            _ => {
+                log::error!("server index and connection options mismatch");
+                self.mode = Mode::default_mode(&BTreeSet::default(), cx);
+                return;
+            }
         });
         self.focus_handle(cx).focus(window);
         cx.notify();
     }
 
-    fn create_ssh_project(
+    fn create_remote_project(
         &mut self,
-        ix: usize,
-        ssh_connection: SshConnection,
+        index: ServerIndex,
+        connection_options: RemoteConnectionOptions,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -544,17 +958,11 @@ impl RemoteServerProjects {
         };
 
         let create_new_window = self.create_new_window;
-        let connection_options: SshConnectionOptions = ssh_connection.into();
         workspace.update(cx, |_, cx| {
             cx.defer_in(window, move |workspace, window, cx| {
                 let app_state = workspace.app_state().clone();
                 workspace.toggle_modal(window, cx, |window, cx| {
-                    RemoteConnectionModal::new(
-                        &RemoteConnectionOptions::Ssh(connection_options.clone()),
-                        Vec::new(),
-                        window,
-                        cx,
-                    )
+                    RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx)
                 });
                 let prompt = workspace
                     .active_modal::<RemoteConnectionModal>(cx)
@@ -563,7 +971,7 @@ impl RemoteServerProjects {
                     .prompt
                     .clone();
 
-                let connect = connect_over_ssh(
+                let connect = connect(
                     ConnectionIdentifier::setup(),
                     connection_options.clone(),
                     prompt,
@@ -624,7 +1032,7 @@ impl RemoteServerProjects {
                             workspace.toggle_modal(window, cx, |window, cx| {
                                 RemoteServerProjects::project_picker(
                                     create_new_window,
-                                    ix,
+                                    index,
                                     connection_options,
                                     project,
                                     home_dir,
@@ -662,7 +1070,7 @@ impl RemoteServerProjects {
                 let index = state.index;
                 self.update_settings_file(cx, move |setting, _| {
                     if let Some(connections) = setting.ssh_connections.as_mut()
-                        && let Some(connection) = connections.get_mut(index)
+                        && let Some(connection) = connections.get_mut(index.0)
                     {
                         connection.nickname = text;
                     }
@@ -670,6 +1078,12 @@ impl RemoteServerProjects {
                 self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
                 self.focus_handle.focus(window);
             }
+            #[cfg(target_os = "windows")]
+            Mode::AddWslDistro(state) => {
+                let delegate = &state.picker.read(cx).delegate;
+                let distro = delegate.selected_distro().unwrap();
+                self.connect_wsl_distro(state.picker.clone(), distro, window, cx);
+            }
         }
     }
 
@@ -702,11 +1116,19 @@ impl RemoteServerProjects {
         cx: &mut Context<Self>,
     ) -> impl IntoElement {
         let connection = ssh_server.connection().into_owned();
-        let (main_label, aux_label) = if let Some(nickname) = connection.nickname.clone() {
-            let aux_label = SharedString::from(format!("({})", connection.host));
-            (nickname.into(), Some(aux_label))
-        } else {
-            (connection.host.clone(), None)
+
+        let (main_label, aux_label, is_wsl) = match &connection {
+            Connection::Ssh(connection) => {
+                if let Some(nickname) = connection.nickname.clone() {
+                    let aux_label = SharedString::from(format!("({})", connection.host));
+                    (nickname.into(), Some(aux_label), false)
+                } else {
+                    (connection.host.clone(), None, false)
+                }
+            }
+            Connection::Wsl(wsl_connection_options) => {
+                (wsl_connection_options.distro_name.clone(), None, true)
+            }
         };
         v_flex()
             .w_full()
@@ -720,11 +1142,23 @@ impl RemoteServerProjects {
                     .gap_1()
                     .overflow_hidden()
                     .child(
-                        div().max_w_96().overflow_hidden().text_ellipsis().child(
-                            Label::new(main_label)
-                                .size(LabelSize::Small)
-                                .color(Color::Muted),
-                        ),
+                        h_flex()
+                            .gap_1()
+                            .max_w_96()
+                            .overflow_hidden()
+                            .text_ellipsis()
+                            .when(is_wsl, |this| {
+                                this.child(
+                                    Label::new("WSL:")
+                                        .size(LabelSize::Small)
+                                        .color(Color::Muted),
+                                )
+                            })
+                            .child(
+                                Label::new(main_label)
+                                    .size(LabelSize::Small)
+                                    .color(Color::Muted),
+                            ),
                     )
                     .children(
                         aux_label.map(|label| {
@@ -738,98 +1172,114 @@ impl RemoteServerProjects {
                     projects,
                     configure,
                     connection,
-                } => List::new()
-                    .empty_message("No projects.")
-                    .children(projects.iter().enumerate().map(|(pix, p)| {
-                        v_flex().gap_0p5().child(self.render_ssh_project(
-                            ix,
-                            ssh_server.clone(),
-                            pix,
-                            p,
-                            window,
-                            cx,
-                        ))
-                    }))
-                    .child(
-                        h_flex()
-                            .id(("new-remote-project-container", ix))
-                            .track_focus(&open_folder.focus_handle)
-                            .anchor_scroll(open_folder.scroll_anchor.clone())
-                            .on_action(cx.listener({
-                                let ssh_connection = connection.clone();
-                                move |this, _: &menu::Confirm, window, cx| {
-                                    this.create_ssh_project(ix, ssh_connection.clone(), window, cx);
-                                }
-                            }))
-                            .child(
-                                ListItem::new(("new-remote-project", ix))
-                                    .toggle_state(
-                                        open_folder.focus_handle.contains_focused(window, cx),
-                                    )
-                                    .inset(true)
-                                    .spacing(ui::ListItemSpacing::Sparse)
-                                    .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
-                                    .child(Label::new("Open Folder"))
-                                    .on_click(cx.listener({
-                                        let ssh_connection = connection.clone();
-                                        move |this, _, window, cx| {
-                                            this.create_ssh_project(
-                                                ix,
-                                                ssh_connection.clone(),
-                                                window,
-                                                cx,
-                                            );
-                                        }
-                                    })),
-                            ),
-                    )
-                    .child(
-                        h_flex()
-                            .id(("server-options-container", ix))
-                            .track_focus(&configure.focus_handle)
-                            .anchor_scroll(configure.scroll_anchor.clone())
-                            .on_action(cx.listener({
-                                let ssh_connection = connection.clone();
-                                move |this, _: &menu::Confirm, window, cx| {
-                                    this.view_server_options(
-                                        (ix, ssh_connection.clone()),
-                                        window,
-                                        cx,
-                                    );
-                                }
-                            }))
-                            .child(
-                                ListItem::new(("server-options", ix))
-                                    .toggle_state(
-                                        configure.focus_handle.contains_focused(window, cx),
-                                    )
-                                    .inset(true)
-                                    .spacing(ui::ListItemSpacing::Sparse)
-                                    .start_slot(Icon::new(IconName::Settings).color(Color::Muted))
-                                    .child(Label::new("View Server Options"))
-                                    .on_click(cx.listener({
-                                        let ssh_connection = connection.clone();
-                                        move |this, _, window, cx| {
-                                            this.view_server_options(
-                                                (ix, ssh_connection.clone()),
-                                                window,
-                                                cx,
-                                            );
-                                        }
-                                    })),
-                            ),
-                    ),
+                    index,
+                } => {
+                    let index = *index;
+                    List::new()
+                        .empty_message("No projects.")
+                        .children(projects.iter().enumerate().map(|(pix, p)| {
+                            v_flex().gap_0p5().child(self.render_ssh_project(
+                                index,
+                                ssh_server.clone(),
+                                pix,
+                                p,
+                                window,
+                                cx,
+                            ))
+                        }))
+                        .child(
+                            h_flex()
+                                .id(("new-remote-project-container", ix))
+                                .track_focus(&open_folder.focus_handle)
+                                .anchor_scroll(open_folder.scroll_anchor.clone())
+                                .on_action(cx.listener({
+                                    let connection = connection.clone();
+                                    move |this, _: &menu::Confirm, window, cx| {
+                                        this.create_remote_project(
+                                            index,
+                                            connection.clone().into(),
+                                            window,
+                                            cx,
+                                        );
+                                    }
+                                }))
+                                .child(
+                                    ListItem::new(("new-remote-project", ix))
+                                        .toggle_state(
+                                            open_folder.focus_handle.contains_focused(window, cx),
+                                        )
+                                        .inset(true)
+                                        .spacing(ui::ListItemSpacing::Sparse)
+                                        .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
+                                        .child(Label::new("Open Folder"))
+                                        .on_click(cx.listener({
+                                            let connection = connection.clone();
+                                            move |this, _, window, cx| {
+                                                this.create_remote_project(
+                                                    index,
+                                                    connection.clone().into(),
+                                                    window,
+                                                    cx,
+                                                );
+                                            }
+                                        })),
+                                ),
+                        )
+                        .child(
+                            h_flex()
+                                .id(("server-options-container", ix))
+                                .track_focus(&configure.focus_handle)
+                                .anchor_scroll(configure.scroll_anchor.clone())
+                                .on_action(cx.listener({
+                                    let connection = connection.clone();
+                                    move |this, _: &menu::Confirm, window, cx| {
+                                        this.view_server_options(
+                                            (index, connection.clone().into()),
+                                            window,
+                                            cx,
+                                        );
+                                    }
+                                }))
+                                .child(
+                                    ListItem::new(("server-options", ix))
+                                        .toggle_state(
+                                            configure.focus_handle.contains_focused(window, cx),
+                                        )
+                                        .inset(true)
+                                        .spacing(ui::ListItemSpacing::Sparse)
+                                        .start_slot(
+                                            Icon::new(IconName::Settings).color(Color::Muted),
+                                        )
+                                        .child(Label::new("View Server Options"))
+                                        .on_click(cx.listener({
+                                            let ssh_connection = connection.clone();
+                                            move |this, _, window, cx| {
+                                                this.view_server_options(
+                                                    (index, ssh_connection.clone().into()),
+                                                    window,
+                                                    cx,
+                                                );
+                                            }
+                                        })),
+                                ),
+                        )
+                }
                 RemoteEntry::SshConfig { open_folder, host } => List::new().child(
                     h_flex()
                         .id(("new-remote-project-container", ix))
                         .track_focus(&open_folder.focus_handle)
                         .anchor_scroll(open_folder.scroll_anchor.clone())
                         .on_action(cx.listener({
-                            let ssh_connection = connection.clone();
+                            let connection = connection.clone();
                             let host = host.clone();
                             move |this, _: &menu::Confirm, window, cx| {
                                 let new_ix = this.create_host_from_ssh_config(&host, cx);
-                                this.create_ssh_project(new_ix, ssh_connection.clone(), window, cx);
+                                this.create_remote_project(
+                                    new_ix.into(),
+                                    connection.clone().into(),
+                                    window,
+                                    cx,
+                                );
                             }
                         }))
                         .child(