remote_servers.rs

   1use crate::{
   2    remote_connections::{
   3        Connection, RemoteConnectionModal, RemoteConnectionPrompt, RemoteSettingsContent,
   4        SshConnection, SshConnectionHeader, SshProject, SshSettings, connect, connect_over_ssh,
   5        open_remote_project,
   6    },
   7    ssh_config::parse_ssh_config_hosts,
   8};
   9use editor::Editor;
  10use file_finder::OpenPathDelegate;
  11use futures::{FutureExt, channel::oneshot, future::Shared, select};
  12use gpui::{
  13    AnyElement, App, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, EventEmitter,
  14    FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task, WeakEntity, Window,
  15    canvas,
  16};
  17use log::info;
  18use paths::{global_ssh_config_file, user_ssh_config_file};
  19use picker::Picker;
  20use project::{Fs, Project};
  21use remote::{
  22    RemoteClient, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions,
  23    remote_client::ConnectionIdentifier,
  24};
  25use settings::{Settings, SettingsStore, update_settings_file, watch_config_file};
  26use smol::stream::StreamExt as _;
  27use std::{
  28    borrow::Cow,
  29    collections::BTreeSet,
  30    path::PathBuf,
  31    rc::Rc,
  32    sync::{
  33        Arc,
  34        atomic::{self, AtomicUsize},
  35    },
  36};
  37use ui::{
  38    IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Navigable, NavigableEntry,
  39    Section, Tooltip, WithScrollbar, prelude::*,
  40};
  41use util::{
  42    ResultExt,
  43    paths::{PathStyle, RemotePathBuf},
  44};
  45use workspace::{
  46    ModalView, OpenOptions, Toast, Workspace,
  47    notifications::{DetachAndPromptErr, NotificationId},
  48    open_remote_project_with_existing_connection,
  49};
  50
  51pub struct RemoteServerProjects {
  52    mode: Mode,
  53    focus_handle: FocusHandle,
  54    workspace: WeakEntity<Workspace>,
  55    retained_connections: Vec<Entity<RemoteClient>>,
  56    ssh_config_updates: Task<()>,
  57    ssh_config_servers: BTreeSet<SharedString>,
  58    create_new_window: bool,
  59    _subscription: Subscription,
  60}
  61
  62struct CreateRemoteServer {
  63    address_editor: Entity<Editor>,
  64    address_error: Option<SharedString>,
  65    ssh_prompt: Option<Entity<RemoteConnectionPrompt>>,
  66    _creating: Option<Task<Option<()>>>,
  67}
  68
  69impl CreateRemoteServer {
  70    fn new(window: &mut Window, cx: &mut App) -> Self {
  71        let address_editor = cx.new(|cx| Editor::single_line(window, cx));
  72        address_editor.update(cx, |this, cx| {
  73            this.focus_handle(cx).focus(window);
  74        });
  75        Self {
  76            address_editor,
  77            address_error: None,
  78            ssh_prompt: None,
  79            _creating: None,
  80        }
  81    }
  82}
  83
  84#[cfg(target_os = "windows")]
  85struct AddWslDistro {
  86    picker: Entity<Picker<WslPickerDelegate>>,
  87    connection_prompt: Option<Entity<RemoteConnectionPrompt>>,
  88    _creating: Option<Task<()>>,
  89}
  90
  91#[cfg(target_os = "windows")]
  92impl AddWslDistro {
  93    fn new(window: &mut Window, cx: &mut Context<RemoteServerProjects>) -> Self {
  94        let delegate = WslPickerDelegate::new();
  95        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
  96
  97        cx.subscribe_in(
  98            &picker,
  99            window,
 100            |this, _, _: &WslDistroSelected, window, cx| {
 101                this.confirm(&menu::Confirm, window, cx);
 102            },
 103        )
 104        .detach();
 105
 106        cx.subscribe_in(
 107            &picker,
 108            window,
 109            |this, _, _: &WslPickerDismissed, window, cx| {
 110                this.cancel(&menu::Cancel, window, cx);
 111            },
 112        )
 113        .detach();
 114
 115        AddWslDistro {
 116            picker,
 117            connection_prompt: None,
 118            _creating: None,
 119        }
 120    }
 121}
 122
 123#[cfg(target_os = "windows")]
 124#[derive(Clone, Debug)]
 125pub struct WslDistroSelected(pub String);
 126
 127#[cfg(target_os = "windows")]
 128#[derive(Clone, Debug)]
 129pub struct WslPickerDismissed;
 130
 131#[cfg(target_os = "windows")]
 132struct WslPickerDelegate {
 133    selected_index: usize,
 134    distro_list: Option<Vec<String>>,
 135    matches: Vec<fuzzy::StringMatch>,
 136}
 137
 138#[cfg(target_os = "windows")]
 139impl WslPickerDelegate {
 140    fn new() -> Self {
 141        WslPickerDelegate {
 142            selected_index: 0,
 143            distro_list: None,
 144            matches: Vec::new(),
 145        }
 146    }
 147
 148    pub fn selected_distro(&self) -> Option<String> {
 149        self.matches
 150            .get(self.selected_index)
 151            .map(|m| m.string.clone())
 152    }
 153}
 154
 155#[cfg(target_os = "windows")]
 156impl WslPickerDelegate {
 157    fn fetch_distros() -> anyhow::Result<Vec<String>> {
 158        use anyhow::Context;
 159        use windows_registry::CURRENT_USER;
 160
 161        let lxss_key = CURRENT_USER
 162            .open("Software\\Microsoft\\Windows\\CurrentVersion\\Lxss")
 163            .context("failed to get lxss wsl key")?;
 164
 165        let distros = lxss_key
 166            .keys()
 167            .context("failed to get wsl distros")?
 168            .filter_map(|key| {
 169                lxss_key
 170                    .open(&key)
 171                    .context("failed to open subkey for distro")
 172                    .log_err()
 173            })
 174            .filter_map(|distro| distro.get_string("DistributionName").ok())
 175            .collect::<Vec<_>>();
 176
 177        Ok(distros)
 178    }
 179}
 180
 181#[cfg(target_os = "windows")]
 182impl EventEmitter<WslDistroSelected> for Picker<WslPickerDelegate> {}
 183
 184#[cfg(target_os = "windows")]
 185impl EventEmitter<WslPickerDismissed> for Picker<WslPickerDelegate> {}
 186
 187#[cfg(target_os = "windows")]
 188impl picker::PickerDelegate for WslPickerDelegate {
 189    type ListItem = ListItem;
 190
 191    fn match_count(&self) -> usize {
 192        self.matches.len()
 193    }
 194
 195    fn selected_index(&self) -> usize {
 196        self.selected_index
 197    }
 198
 199    fn set_selected_index(
 200        &mut self,
 201        ix: usize,
 202        _window: &mut Window,
 203        cx: &mut Context<Picker<Self>>,
 204    ) {
 205        self.selected_index = ix;
 206        cx.notify();
 207    }
 208
 209    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 210        Arc::from("Enter WSL distro name")
 211    }
 212
 213    fn update_matches(
 214        &mut self,
 215        query: String,
 216        _window: &mut Window,
 217        cx: &mut Context<Picker<Self>>,
 218    ) -> Task<()> {
 219        use fuzzy::StringMatchCandidate;
 220
 221        let needs_fetch = self.distro_list.is_none();
 222        if needs_fetch {
 223            let distros = Self::fetch_distros().log_err();
 224            self.distro_list = distros;
 225        }
 226
 227        if let Some(distro_list) = &self.distro_list {
 228            use ordered_float::OrderedFloat;
 229
 230            let candidates = distro_list
 231                .iter()
 232                .enumerate()
 233                .map(|(id, distro)| StringMatchCandidate::new(id, distro))
 234                .collect::<Vec<_>>();
 235
 236            let query = query.trim_start();
 237            let smart_case = query.chars().any(|c| c.is_uppercase());
 238            self.matches = smol::block_on(fuzzy::match_strings(
 239                candidates.as_slice(),
 240                query,
 241                smart_case,
 242                true,
 243                100,
 244                &Default::default(),
 245                cx.background_executor().clone(),
 246            ));
 247            self.matches.sort_unstable_by_key(|m| m.candidate_id);
 248
 249            self.selected_index = self
 250                .matches
 251                .iter()
 252                .enumerate()
 253                .rev()
 254                .max_by_key(|(_, m)| OrderedFloat(m.score))
 255                .map(|(index, _)| index)
 256                .unwrap_or(0);
 257        }
 258
 259        Task::ready(())
 260    }
 261
 262    fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
 263        if let Some(distro) = self.matches.get(self.selected_index) {
 264            cx.emit(WslDistroSelected(distro.string.clone()));
 265        }
 266    }
 267
 268    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
 269        cx.emit(WslPickerDismissed);
 270    }
 271
 272    fn render_match(
 273        &self,
 274        ix: usize,
 275        selected: bool,
 276        _: &mut Window,
 277        _: &mut Context<Picker<Self>>,
 278    ) -> Option<Self::ListItem> {
 279        use ui::HighlightedLabel;
 280
 281        let matched = self.matches.get(ix)?;
 282        Some(
 283            ListItem::new(ix)
 284                .toggle_state(selected)
 285                .inset(true)
 286                .spacing(ui::ListItemSpacing::Sparse)
 287                .child(
 288                    h_flex()
 289                        .flex_grow()
 290                        .gap_3()
 291                        .child(Icon::new(IconName::Server))
 292                        .child(v_flex().child(HighlightedLabel::new(
 293                            matched.string.clone(),
 294                            matched.positions.clone(),
 295                        ))),
 296                ),
 297        )
 298    }
 299}
 300
 301enum ProjectPickerData {
 302    Ssh {
 303        connection_string: SharedString,
 304        nickname: Option<SharedString>,
 305    },
 306    Wsl {
 307        distro_name: SharedString,
 308    },
 309}
 310
 311struct ProjectPicker {
 312    data: ProjectPickerData,
 313    picker: Entity<Picker<OpenPathDelegate>>,
 314    _path_task: Shared<Task<Option<()>>>,
 315}
 316
 317struct EditNicknameState {
 318    index: SshServerIndex,
 319    editor: Entity<Editor>,
 320}
 321
 322impl EditNicknameState {
 323    fn new(index: SshServerIndex, window: &mut Window, cx: &mut App) -> Self {
 324        let this = Self {
 325            index,
 326            editor: cx.new(|cx| Editor::single_line(window, cx)),
 327        };
 328        let starting_text = SshSettings::get_global(cx)
 329            .ssh_connections()
 330            .nth(index.0)
 331            .and_then(|state| state.nickname)
 332            .filter(|text| !text.is_empty());
 333        this.editor.update(cx, |this, cx| {
 334            this.set_placeholder_text("Add a nickname for this server", window, cx);
 335            if let Some(starting_text) = starting_text {
 336                this.set_text(starting_text, window, cx);
 337            }
 338        });
 339        this.editor.focus_handle(cx).focus(window);
 340        this
 341    }
 342}
 343
 344impl Focusable for ProjectPicker {
 345    fn focus_handle(&self, cx: &App) -> FocusHandle {
 346        self.picker.focus_handle(cx)
 347    }
 348}
 349
 350impl ProjectPicker {
 351    fn new(
 352        create_new_window: bool,
 353        index: ServerIndex,
 354        connection: RemoteConnectionOptions,
 355        project: Entity<Project>,
 356        home_dir: RemotePathBuf,
 357        path_style: PathStyle,
 358        workspace: WeakEntity<Workspace>,
 359        window: &mut Window,
 360        cx: &mut Context<RemoteServerProjects>,
 361    ) -> Entity<Self> {
 362        let (tx, rx) = oneshot::channel();
 363        let lister = project::DirectoryLister::Project(project.clone());
 364        let delegate = file_finder::OpenPathDelegate::new(tx, lister, false, path_style);
 365
 366        let picker = cx.new(|cx| {
 367            let picker = Picker::uniform_list(delegate, window, cx)
 368                .width(rems(34.))
 369                .modal(false);
 370            picker.set_query(home_dir.to_string(), window, cx);
 371            picker
 372        });
 373
 374        let data = match &connection {
 375            RemoteConnectionOptions::Ssh(connection) => ProjectPickerData::Ssh {
 376                connection_string: connection.connection_string().into(),
 377                nickname: connection.nickname.clone().map(|nick| nick.into()),
 378            },
 379            RemoteConnectionOptions::Wsl(connection) => ProjectPickerData::Wsl {
 380                distro_name: connection.distro_name.clone().into(),
 381            },
 382        };
 383        let _path_task = cx
 384            .spawn_in(window, {
 385                let workspace = workspace;
 386                async move |this, cx| {
 387                    let Ok(Some(paths)) = rx.await else {
 388                        workspace
 389                            .update_in(cx, |workspace, window, cx| {
 390                                let fs = workspace.project().read(cx).fs().clone();
 391                                let weak = cx.entity().downgrade();
 392                                workspace.toggle_modal(window, cx, |window, cx| {
 393                                    RemoteServerProjects::new(
 394                                        create_new_window,
 395                                        fs,
 396                                        window,
 397                                        weak,
 398                                        cx,
 399                                    )
 400                                });
 401                            })
 402                            .log_err()?;
 403                        return None;
 404                    };
 405
 406                    let app_state = workspace
 407                        .read_with(cx, |workspace, _| workspace.app_state().clone())
 408                        .ok()?;
 409
 410                    cx.update(|_, cx| {
 411                        let fs = app_state.fs.clone();
 412                        update_settings_file::<SshSettings>(fs, cx, {
 413                            let paths = paths
 414                                .iter()
 415                                .map(|path| path.to_string_lossy().to_string())
 416                                .collect();
 417                            move |setting, _| match index {
 418                                ServerIndex::Ssh(index) => {
 419                                    if let Some(server) = setting
 420                                        .ssh_connections
 421                                        .as_mut()
 422                                        .and_then(|connections| connections.get_mut(index.0))
 423                                    {
 424                                        server.projects.insert(SshProject { paths });
 425                                    };
 426                                }
 427                                ServerIndex::Wsl(index) => {
 428                                    if let Some(server) = setting
 429                                        .wsl_connections
 430                                        .as_mut()
 431                                        .and_then(|connections| connections.get_mut(index.0))
 432                                    {
 433                                        server.projects.insert(SshProject { paths });
 434                                    };
 435                                }
 436                            }
 437                        });
 438                    })
 439                    .log_err();
 440
 441                    let options = cx
 442                        .update(|_, cx| (app_state.build_window_options)(None, cx))
 443                        .log_err()?;
 444                    let window = cx
 445                        .open_window(options, |window, cx| {
 446                            cx.new(|cx| {
 447                                telemetry::event!("SSH Project Created");
 448                                Workspace::new(None, project.clone(), app_state.clone(), window, cx)
 449                            })
 450                        })
 451                        .log_err()?;
 452
 453                    open_remote_project_with_existing_connection(
 454                        connection, project, paths, app_state, window, cx,
 455                    )
 456                    .await
 457                    .log_err();
 458
 459                    this.update(cx, |_, cx| {
 460                        cx.emit(DismissEvent);
 461                    })
 462                    .ok();
 463                    Some(())
 464                }
 465            })
 466            .shared();
 467        cx.new(|_| Self {
 468            _path_task,
 469            picker,
 470            data,
 471        })
 472    }
 473}
 474
 475impl gpui::Render for ProjectPicker {
 476    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 477        v_flex()
 478            .child(match &self.data {
 479                ProjectPickerData::Ssh {
 480                    connection_string,
 481                    nickname,
 482                } => SshConnectionHeader {
 483                    connection_string: connection_string.clone(),
 484                    paths: Default::default(),
 485                    nickname: nickname.clone(),
 486                }
 487                .render(window, cx),
 488                ProjectPickerData::Wsl { distro_name } => SshConnectionHeader {
 489                    connection_string: distro_name.clone(),
 490                    paths: Default::default(),
 491                    nickname: None,
 492                }
 493                .render(window, cx),
 494            })
 495            .child(
 496                div()
 497                    .border_t_1()
 498                    .border_color(cx.theme().colors().border_variant)
 499                    .child(self.picker.clone()),
 500            )
 501    }
 502}
 503
 504#[repr(transparent)]
 505#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
 506struct SshServerIndex(usize);
 507impl std::fmt::Display for SshServerIndex {
 508    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 509        self.0.fmt(f)
 510    }
 511}
 512
 513#[repr(transparent)]
 514#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
 515struct WslServerIndex(usize);
 516impl std::fmt::Display for WslServerIndex {
 517    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 518        self.0.fmt(f)
 519    }
 520}
 521
 522#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
 523enum ServerIndex {
 524    Ssh(SshServerIndex),
 525    Wsl(WslServerIndex),
 526}
 527impl From<SshServerIndex> for ServerIndex {
 528    fn from(index: SshServerIndex) -> Self {
 529        Self::Ssh(index)
 530    }
 531}
 532impl From<WslServerIndex> for ServerIndex {
 533    fn from(index: WslServerIndex) -> Self {
 534        Self::Wsl(index)
 535    }
 536}
 537
 538#[derive(Clone)]
 539enum RemoteEntry {
 540    Project {
 541        open_folder: NavigableEntry,
 542        projects: Vec<(NavigableEntry, SshProject)>,
 543        configure: NavigableEntry,
 544        connection: Connection,
 545        index: ServerIndex,
 546    },
 547    SshConfig {
 548        open_folder: NavigableEntry,
 549        host: SharedString,
 550    },
 551}
 552
 553impl RemoteEntry {
 554    fn is_from_zed(&self) -> bool {
 555        matches!(self, Self::Project { .. })
 556    }
 557
 558    fn connection(&self) -> Cow<'_, Connection> {
 559        match self {
 560            Self::Project { connection, .. } => Cow::Borrowed(connection),
 561            Self::SshConfig { host, .. } => Cow::Owned(
 562                SshConnection {
 563                    host: host.clone(),
 564                    ..SshConnection::default()
 565                }
 566                .into(),
 567            ),
 568        }
 569    }
 570}
 571
 572#[derive(Clone)]
 573struct DefaultState {
 574    scroll_handle: ScrollHandle,
 575    add_new_server: NavigableEntry,
 576    add_new_wsl: NavigableEntry,
 577    servers: Vec<RemoteEntry>,
 578}
 579
 580impl DefaultState {
 581    fn new(ssh_config_servers: &BTreeSet<SharedString>, cx: &mut App) -> Self {
 582        let handle = ScrollHandle::new();
 583        let add_new_server = NavigableEntry::new(&handle, cx);
 584        let add_new_wsl = NavigableEntry::new(&handle, cx);
 585
 586        let ssh_settings = SshSettings::get_global(cx);
 587        let read_ssh_config = ssh_settings.read_ssh_config;
 588
 589        let ssh_servers = ssh_settings
 590            .ssh_connections()
 591            .enumerate()
 592            .map(|(index, connection)| {
 593                let open_folder = NavigableEntry::new(&handle, cx);
 594                let configure = NavigableEntry::new(&handle, cx);
 595                let projects = connection
 596                    .projects
 597                    .iter()
 598                    .map(|project| (NavigableEntry::new(&handle, cx), project.clone()))
 599                    .collect();
 600                RemoteEntry::Project {
 601                    open_folder,
 602                    configure,
 603                    projects,
 604                    index: ServerIndex::Ssh(SshServerIndex(index)),
 605                    connection: connection.into(),
 606                }
 607            });
 608
 609        let wsl_servers = ssh_settings
 610            .wsl_connections()
 611            .enumerate()
 612            .map(|(index, connection)| {
 613                let open_folder = NavigableEntry::new(&handle, cx);
 614                let configure = NavigableEntry::new(&handle, cx);
 615                let projects = connection
 616                    .projects
 617                    .iter()
 618                    .map(|project| (NavigableEntry::new(&handle, cx), project.clone()))
 619                    .collect();
 620                RemoteEntry::Project {
 621                    open_folder,
 622                    configure,
 623                    projects,
 624                    index: ServerIndex::Wsl(WslServerIndex(index)),
 625                    connection: connection.into(),
 626                }
 627            });
 628
 629        let mut servers = ssh_servers.chain(wsl_servers).collect::<Vec<RemoteEntry>>();
 630
 631        if read_ssh_config {
 632            let mut extra_servers_from_config = ssh_config_servers.clone();
 633            for server in &servers {
 634                if let RemoteEntry::Project {
 635                    connection: Connection::Ssh(ssh_options),
 636                    ..
 637                } = server
 638                {
 639                    extra_servers_from_config.remove(&SharedString::new(ssh_options.host.clone()));
 640                }
 641            }
 642            servers.extend(extra_servers_from_config.into_iter().map(|host| {
 643                RemoteEntry::SshConfig {
 644                    open_folder: NavigableEntry::new(&handle, cx),
 645                    host,
 646                }
 647            }));
 648        }
 649
 650        Self {
 651            scroll_handle: handle,
 652            add_new_server,
 653            add_new_wsl,
 654            servers,
 655        }
 656    }
 657}
 658
 659#[derive(Clone)]
 660enum ViewServerOptionsState {
 661    Ssh {
 662        connection: SshConnectionOptions,
 663        server_index: SshServerIndex,
 664        entries: [NavigableEntry; 4],
 665    },
 666    Wsl {
 667        connection: WslConnectionOptions,
 668        server_index: WslServerIndex,
 669        entries: [NavigableEntry; 2],
 670    },
 671}
 672
 673impl ViewServerOptionsState {
 674    fn entries(&self) -> &[NavigableEntry] {
 675        match self {
 676            Self::Ssh { entries, .. } => entries,
 677            Self::Wsl { entries, .. } => entries,
 678        }
 679    }
 680}
 681
 682enum Mode {
 683    Default(DefaultState),
 684    ViewServerOptions(ViewServerOptionsState),
 685    EditNickname(EditNicknameState),
 686    ProjectPicker(Entity<ProjectPicker>),
 687    CreateRemoteServer(CreateRemoteServer),
 688    #[cfg(target_os = "windows")]
 689    AddWslDistro(AddWslDistro),
 690}
 691
 692impl Mode {
 693    fn default_mode(ssh_config_servers: &BTreeSet<SharedString>, cx: &mut App) -> Self {
 694        Self::Default(DefaultState::new(ssh_config_servers, cx))
 695    }
 696}
 697
 698impl RemoteServerProjects {
 699    pub fn new(
 700        create_new_window: bool,
 701        fs: Arc<dyn Fs>,
 702        window: &mut Window,
 703        workspace: WeakEntity<Workspace>,
 704        cx: &mut Context<Self>,
 705    ) -> Self {
 706        let focus_handle = cx.focus_handle();
 707        let mut read_ssh_config = SshSettings::get_global(cx).read_ssh_config;
 708        let ssh_config_updates = if read_ssh_config {
 709            spawn_ssh_config_watch(fs.clone(), cx)
 710        } else {
 711            Task::ready(())
 712        };
 713
 714        let mut base_style = window.text_style();
 715        base_style.refine(&gpui::TextStyleRefinement {
 716            color: Some(cx.theme().colors().editor_foreground),
 717            ..Default::default()
 718        });
 719
 720        let _subscription =
 721            cx.observe_global_in::<SettingsStore>(window, move |recent_projects, _, cx| {
 722                let new_read_ssh_config = SshSettings::get_global(cx).read_ssh_config;
 723                if read_ssh_config != new_read_ssh_config {
 724                    read_ssh_config = new_read_ssh_config;
 725                    if read_ssh_config {
 726                        recent_projects.ssh_config_updates = spawn_ssh_config_watch(fs.clone(), cx);
 727                    } else {
 728                        recent_projects.ssh_config_servers.clear();
 729                        recent_projects.ssh_config_updates = Task::ready(());
 730                    }
 731                }
 732            });
 733
 734        Self {
 735            mode: Mode::default_mode(&BTreeSet::new(), cx),
 736            focus_handle,
 737            workspace,
 738            retained_connections: Vec::new(),
 739            ssh_config_updates,
 740            ssh_config_servers: BTreeSet::new(),
 741            create_new_window,
 742            _subscription,
 743        }
 744    }
 745
 746    fn project_picker(
 747        create_new_window: bool,
 748        index: ServerIndex,
 749        connection_options: remote::RemoteConnectionOptions,
 750        project: Entity<Project>,
 751        home_dir: RemotePathBuf,
 752        path_style: PathStyle,
 753        window: &mut Window,
 754        cx: &mut Context<Self>,
 755        workspace: WeakEntity<Workspace>,
 756    ) -> Self {
 757        let fs = project.read(cx).fs().clone();
 758        let mut this = Self::new(create_new_window, fs, window, workspace.clone(), cx);
 759        this.mode = Mode::ProjectPicker(ProjectPicker::new(
 760            create_new_window,
 761            index,
 762            connection_options,
 763            project,
 764            home_dir,
 765            path_style,
 766            workspace,
 767            window,
 768            cx,
 769        ));
 770        cx.notify();
 771
 772        this
 773    }
 774
 775    fn create_ssh_server(
 776        &mut self,
 777        editor: Entity<Editor>,
 778        window: &mut Window,
 779        cx: &mut Context<Self>,
 780    ) {
 781        let input = get_text(&editor, cx);
 782        if input.is_empty() {
 783            return;
 784        }
 785
 786        let connection_options = match SshConnectionOptions::parse_command_line(&input) {
 787            Ok(c) => c,
 788            Err(e) => {
 789                self.mode = Mode::CreateRemoteServer(CreateRemoteServer {
 790                    address_editor: editor,
 791                    address_error: Some(format!("could not parse: {:?}", e).into()),
 792                    ssh_prompt: None,
 793                    _creating: None,
 794                });
 795                return;
 796            }
 797        };
 798        let ssh_prompt = cx.new(|cx| {
 799            RemoteConnectionPrompt::new(
 800                connection_options.connection_string(),
 801                connection_options.nickname.clone(),
 802                window,
 803                cx,
 804            )
 805        });
 806
 807        let connection = connect_over_ssh(
 808            ConnectionIdentifier::setup(),
 809            connection_options.clone(),
 810            ssh_prompt.clone(),
 811            window,
 812            cx,
 813        )
 814        .prompt_err("Failed to connect", window, cx, |_, _, _| None);
 815
 816        let address_editor = editor.clone();
 817        let creating = cx.spawn_in(window, async move |this, cx| {
 818            match connection.await {
 819                Some(Some(client)) => this
 820                    .update_in(cx, |this, window, cx| {
 821                        info!("ssh server created");
 822                        telemetry::event!("SSH Server Created");
 823                        this.retained_connections.push(client);
 824                        this.add_ssh_server(connection_options, cx);
 825                        this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
 826                        this.focus_handle(cx).focus(window);
 827                        cx.notify()
 828                    })
 829                    .log_err(),
 830                _ => this
 831                    .update(cx, |this, cx| {
 832                        address_editor.update(cx, |this, _| {
 833                            this.set_read_only(false);
 834                        });
 835                        this.mode = Mode::CreateRemoteServer(CreateRemoteServer {
 836                            address_editor,
 837                            address_error: None,
 838                            ssh_prompt: None,
 839                            _creating: None,
 840                        });
 841                        cx.notify()
 842                    })
 843                    .log_err(),
 844            };
 845            None
 846        });
 847
 848        editor.update(cx, |this, _| {
 849            this.set_read_only(true);
 850        });
 851        self.mode = Mode::CreateRemoteServer(CreateRemoteServer {
 852            address_editor: editor,
 853            address_error: None,
 854            ssh_prompt: Some(ssh_prompt),
 855            _creating: Some(creating),
 856        });
 857    }
 858
 859    #[cfg(target_os = "windows")]
 860    fn connect_wsl_distro(
 861        &mut self,
 862        picker: Entity<Picker<WslPickerDelegate>>,
 863        distro: String,
 864        window: &mut Window,
 865        cx: &mut Context<Self>,
 866    ) {
 867        let connection_options = WslConnectionOptions {
 868            distro_name: distro,
 869            user: None,
 870        };
 871
 872        let prompt = cx.new(|cx| {
 873            RemoteConnectionPrompt::new(connection_options.distro_name.clone(), None, window, cx)
 874        });
 875        let connection = connect(
 876            ConnectionIdentifier::setup(),
 877            connection_options.clone().into(),
 878            prompt.clone(),
 879            window,
 880            cx,
 881        )
 882        .prompt_err("Failed to connect", window, cx, |_, _, _| None);
 883
 884        let wsl_picker = picker.clone();
 885        let creating = cx.spawn_in(window, async move |this, cx| {
 886            match connection.await {
 887                Some(Some(client)) => this
 888                    .update_in(cx, |this, window, cx| {
 889                        telemetry::event!("WSL Distro Added");
 890                        this.retained_connections.push(client);
 891                        this.add_wsl_distro(connection_options, cx);
 892                        this.mode = Mode::default_mode(&BTreeSet::new(), cx);
 893                        this.focus_handle(cx).focus(window);
 894                        cx.notify()
 895                    })
 896                    .log_err(),
 897                _ => this
 898                    .update(cx, |this, cx| {
 899                        this.mode = Mode::AddWslDistro(AddWslDistro {
 900                            picker: wsl_picker,
 901                            connection_prompt: None,
 902                            _creating: None,
 903                        });
 904                        cx.notify()
 905                    })
 906                    .log_err(),
 907            };
 908            ()
 909        });
 910
 911        self.mode = Mode::AddWslDistro(AddWslDistro {
 912            picker,
 913            connection_prompt: Some(prompt),
 914            _creating: Some(creating),
 915        });
 916    }
 917
 918    fn view_server_options(
 919        &mut self,
 920        (server_index, connection): (ServerIndex, RemoteConnectionOptions),
 921        window: &mut Window,
 922        cx: &mut Context<Self>,
 923    ) {
 924        self.mode = Mode::ViewServerOptions(match (server_index, connection) {
 925            (ServerIndex::Ssh(server_index), RemoteConnectionOptions::Ssh(connection)) => {
 926                ViewServerOptionsState::Ssh {
 927                    connection,
 928                    server_index,
 929                    entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)),
 930                }
 931            }
 932            (ServerIndex::Wsl(server_index), RemoteConnectionOptions::Wsl(connection)) => {
 933                ViewServerOptionsState::Wsl {
 934                    connection,
 935                    server_index,
 936                    entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)),
 937                }
 938            }
 939            _ => {
 940                log::error!("server index and connection options mismatch");
 941                self.mode = Mode::default_mode(&BTreeSet::default(), cx);
 942                return;
 943            }
 944        });
 945        self.focus_handle(cx).focus(window);
 946        cx.notify();
 947    }
 948
 949    fn create_remote_project(
 950        &mut self,
 951        index: ServerIndex,
 952        connection_options: RemoteConnectionOptions,
 953        window: &mut Window,
 954        cx: &mut Context<Self>,
 955    ) {
 956        let Some(workspace) = self.workspace.upgrade() else {
 957            return;
 958        };
 959
 960        let create_new_window = self.create_new_window;
 961        workspace.update(cx, |_, cx| {
 962            cx.defer_in(window, move |workspace, window, cx| {
 963                let app_state = workspace.app_state().clone();
 964                workspace.toggle_modal(window, cx, |window, cx| {
 965                    RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx)
 966                });
 967                let prompt = workspace
 968                    .active_modal::<RemoteConnectionModal>(cx)
 969                    .unwrap()
 970                    .read(cx)
 971                    .prompt
 972                    .clone();
 973
 974                let connect = connect(
 975                    ConnectionIdentifier::setup(),
 976                    connection_options.clone(),
 977                    prompt,
 978                    window,
 979                    cx,
 980                )
 981                .prompt_err("Failed to connect", window, cx, |_, _, _| None);
 982
 983                cx.spawn_in(window, async move |workspace, cx| {
 984                    let session = connect.await;
 985
 986                    workspace.update(cx, |workspace, cx| {
 987                        if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) {
 988                            prompt.update(cx, |prompt, cx| prompt.finished(cx))
 989                        }
 990                    })?;
 991
 992                    let Some(Some(session)) = session else {
 993                        return workspace.update_in(cx, |workspace, window, cx| {
 994                            let weak = cx.entity().downgrade();
 995                            let fs = workspace.project().read(cx).fs().clone();
 996                            workspace.toggle_modal(window, cx, |window, cx| {
 997                                RemoteServerProjects::new(create_new_window, fs, window, weak, cx)
 998                            });
 999                        });
1000                    };
1001
1002                    let (path_style, project) = cx.update(|_, cx| {
1003                        (
1004                            session.read(cx).path_style(),
1005                            project::Project::remote(
1006                                session,
1007                                app_state.client.clone(),
1008                                app_state.node_runtime.clone(),
1009                                app_state.user_store.clone(),
1010                                app_state.languages.clone(),
1011                                app_state.fs.clone(),
1012                                cx,
1013                            ),
1014                        )
1015                    })?;
1016
1017                    let home_dir = project
1018                        .read_with(cx, |project, cx| project.resolve_abs_path("~", cx))?
1019                        .await
1020                        .and_then(|path| path.into_abs_path())
1021                        .map(|path| RemotePathBuf::new(path, path_style))
1022                        .unwrap_or_else(|| match path_style {
1023                            PathStyle::Posix => RemotePathBuf::from_str("/", PathStyle::Posix),
1024                            PathStyle::Windows => {
1025                                RemotePathBuf::from_str("C:\\", PathStyle::Windows)
1026                            }
1027                        });
1028
1029                    workspace
1030                        .update_in(cx, |workspace, window, cx| {
1031                            let weak = cx.entity().downgrade();
1032                            workspace.toggle_modal(window, cx, |window, cx| {
1033                                RemoteServerProjects::project_picker(
1034                                    create_new_window,
1035                                    index,
1036                                    connection_options,
1037                                    project,
1038                                    home_dir,
1039                                    path_style,
1040                                    window,
1041                                    cx,
1042                                    weak,
1043                                )
1044                            });
1045                        })
1046                        .ok();
1047                    Ok(())
1048                })
1049                .detach();
1050            })
1051        })
1052    }
1053
1054    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
1055        match &self.mode {
1056            Mode::Default(_) | Mode::ViewServerOptions(_) => {}
1057            Mode::ProjectPicker(_) => {}
1058            Mode::CreateRemoteServer(state) => {
1059                if let Some(prompt) = state.ssh_prompt.as_ref() {
1060                    prompt.update(cx, |prompt, cx| {
1061                        prompt.confirm(window, cx);
1062                    });
1063                    return;
1064                }
1065
1066                self.create_ssh_server(state.address_editor.clone(), window, cx);
1067            }
1068            Mode::EditNickname(state) => {
1069                let text = Some(state.editor.read(cx).text(cx)).filter(|text| !text.is_empty());
1070                let index = state.index;
1071                self.update_settings_file(cx, move |setting, _| {
1072                    if let Some(connections) = setting.ssh_connections.as_mut()
1073                        && let Some(connection) = connections.get_mut(index.0)
1074                    {
1075                        connection.nickname = text;
1076                    }
1077                });
1078                self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
1079                self.focus_handle.focus(window);
1080            }
1081            #[cfg(target_os = "windows")]
1082            Mode::AddWslDistro(state) => {
1083                let delegate = &state.picker.read(cx).delegate;
1084                let distro = delegate.selected_distro().unwrap();
1085                self.connect_wsl_distro(state.picker.clone(), distro, window, cx);
1086            }
1087        }
1088    }
1089
1090    fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
1091        match &self.mode {
1092            Mode::Default(_) => cx.emit(DismissEvent),
1093            Mode::CreateRemoteServer(state) if state.ssh_prompt.is_some() => {
1094                let new_state = CreateRemoteServer::new(window, cx);
1095                let old_prompt = state.address_editor.read(cx).text(cx);
1096                new_state.address_editor.update(cx, |this, cx| {
1097                    this.set_text(old_prompt, window, cx);
1098                });
1099
1100                self.mode = Mode::CreateRemoteServer(new_state);
1101                cx.notify();
1102            }
1103            _ => {
1104                self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
1105                self.focus_handle(cx).focus(window);
1106                cx.notify();
1107            }
1108        }
1109    }
1110
1111    fn render_ssh_connection(
1112        &mut self,
1113        ix: usize,
1114        ssh_server: RemoteEntry,
1115        window: &mut Window,
1116        cx: &mut Context<Self>,
1117    ) -> impl IntoElement {
1118        let connection = ssh_server.connection().into_owned();
1119
1120        let (main_label, aux_label, is_wsl) = match &connection {
1121            Connection::Ssh(connection) => {
1122                if let Some(nickname) = connection.nickname.clone() {
1123                    let aux_label = SharedString::from(format!("({})", connection.host));
1124                    (nickname.into(), Some(aux_label), false)
1125                } else {
1126                    (connection.host.clone(), None, false)
1127                }
1128            }
1129            Connection::Wsl(wsl_connection_options) => {
1130                (wsl_connection_options.distro_name.clone(), None, true)
1131            }
1132        };
1133        v_flex()
1134            .w_full()
1135            .child(ListSeparator)
1136            .child(
1137                h_flex()
1138                    .group("ssh-server")
1139                    .w_full()
1140                    .pt_0p5()
1141                    .px_3()
1142                    .gap_1()
1143                    .overflow_hidden()
1144                    .child(
1145                        h_flex()
1146                            .gap_1()
1147                            .max_w_96()
1148                            .overflow_hidden()
1149                            .text_ellipsis()
1150                            .when(is_wsl, |this| {
1151                                this.child(
1152                                    Label::new("WSL:")
1153                                        .size(LabelSize::Small)
1154                                        .color(Color::Muted),
1155                                )
1156                            })
1157                            .child(
1158                                Label::new(main_label)
1159                                    .size(LabelSize::Small)
1160                                    .color(Color::Muted),
1161                            ),
1162                    )
1163                    .children(
1164                        aux_label.map(|label| {
1165                            Label::new(label).size(LabelSize::Small).color(Color::Muted)
1166                        }),
1167                    ),
1168            )
1169            .child(match &ssh_server {
1170                RemoteEntry::Project {
1171                    open_folder,
1172                    projects,
1173                    configure,
1174                    connection,
1175                    index,
1176                } => {
1177                    let index = *index;
1178                    List::new()
1179                        .empty_message("No projects.")
1180                        .children(projects.iter().enumerate().map(|(pix, p)| {
1181                            v_flex().gap_0p5().child(self.render_ssh_project(
1182                                index,
1183                                ssh_server.clone(),
1184                                pix,
1185                                p,
1186                                window,
1187                                cx,
1188                            ))
1189                        }))
1190                        .child(
1191                            h_flex()
1192                                .id(("new-remote-project-container", ix))
1193                                .track_focus(&open_folder.focus_handle)
1194                                .anchor_scroll(open_folder.scroll_anchor.clone())
1195                                .on_action(cx.listener({
1196                                    let connection = connection.clone();
1197                                    move |this, _: &menu::Confirm, window, cx| {
1198                                        this.create_remote_project(
1199                                            index,
1200                                            connection.clone().into(),
1201                                            window,
1202                                            cx,
1203                                        );
1204                                    }
1205                                }))
1206                                .child(
1207                                    ListItem::new(("new-remote-project", ix))
1208                                        .toggle_state(
1209                                            open_folder.focus_handle.contains_focused(window, cx),
1210                                        )
1211                                        .inset(true)
1212                                        .spacing(ui::ListItemSpacing::Sparse)
1213                                        .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
1214                                        .child(Label::new("Open Folder"))
1215                                        .on_click(cx.listener({
1216                                            let connection = connection.clone();
1217                                            move |this, _, window, cx| {
1218                                                this.create_remote_project(
1219                                                    index,
1220                                                    connection.clone().into(),
1221                                                    window,
1222                                                    cx,
1223                                                );
1224                                            }
1225                                        })),
1226                                ),
1227                        )
1228                        .child(
1229                            h_flex()
1230                                .id(("server-options-container", ix))
1231                                .track_focus(&configure.focus_handle)
1232                                .anchor_scroll(configure.scroll_anchor.clone())
1233                                .on_action(cx.listener({
1234                                    let connection = connection.clone();
1235                                    move |this, _: &menu::Confirm, window, cx| {
1236                                        this.view_server_options(
1237                                            (index, connection.clone().into()),
1238                                            window,
1239                                            cx,
1240                                        );
1241                                    }
1242                                }))
1243                                .child(
1244                                    ListItem::new(("server-options", ix))
1245                                        .toggle_state(
1246                                            configure.focus_handle.contains_focused(window, cx),
1247                                        )
1248                                        .inset(true)
1249                                        .spacing(ui::ListItemSpacing::Sparse)
1250                                        .start_slot(
1251                                            Icon::new(IconName::Settings).color(Color::Muted),
1252                                        )
1253                                        .child(Label::new("View Server Options"))
1254                                        .on_click(cx.listener({
1255                                            let ssh_connection = connection.clone();
1256                                            move |this, _, window, cx| {
1257                                                this.view_server_options(
1258                                                    (index, ssh_connection.clone().into()),
1259                                                    window,
1260                                                    cx,
1261                                                );
1262                                            }
1263                                        })),
1264                                ),
1265                        )
1266                }
1267                RemoteEntry::SshConfig { open_folder, host } => List::new().child(
1268                    h_flex()
1269                        .id(("new-remote-project-container", ix))
1270                        .track_focus(&open_folder.focus_handle)
1271                        .anchor_scroll(open_folder.scroll_anchor.clone())
1272                        .on_action(cx.listener({
1273                            let connection = connection.clone();
1274                            let host = host.clone();
1275                            move |this, _: &menu::Confirm, window, cx| {
1276                                let new_ix = this.create_host_from_ssh_config(&host, cx);
1277                                this.create_remote_project(
1278                                    new_ix.into(),
1279                                    connection.clone().into(),
1280                                    window,
1281                                    cx,
1282                                );
1283                            }
1284                        }))
1285                        .child(
1286                            ListItem::new(("new-remote-project", ix))
1287                                .toggle_state(open_folder.focus_handle.contains_focused(window, cx))
1288                                .inset(true)
1289                                .spacing(ui::ListItemSpacing::Sparse)
1290                                .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
1291                                .child(Label::new("Open Folder"))
1292                                .on_click(cx.listener({
1293                                    let host = host.clone();
1294                                    move |this, _, window, cx| {
1295                                        let new_ix = this.create_host_from_ssh_config(&host, cx);
1296                                        this.create_remote_project(
1297                                            new_ix.into(),
1298                                            connection.clone().into(),
1299                                            window,
1300                                            cx,
1301                                        );
1302                                    }
1303                                })),
1304                        ),
1305                ),
1306            })
1307    }
1308
1309    fn render_ssh_project(
1310        &mut self,
1311        server_ix: ServerIndex,
1312        server: RemoteEntry,
1313        ix: usize,
1314        (navigation, project): &(NavigableEntry, SshProject),
1315        window: &mut Window,
1316        cx: &mut Context<Self>,
1317    ) -> impl IntoElement {
1318        let create_new_window = self.create_new_window;
1319        let is_from_zed = server.is_from_zed();
1320        let element_id_base = SharedString::from(format!(
1321            "remote-project-{}",
1322            match server_ix {
1323                ServerIndex::Ssh(index) => format!("ssh-{index}"),
1324                ServerIndex::Wsl(index) => format!("wsl-{index}"),
1325            }
1326        ));
1327        let container_element_id_base =
1328            SharedString::from(format!("remote-project-container-{element_id_base}"));
1329
1330        let callback = Rc::new({
1331            let project = project.clone();
1332            move |remote_server_projects: &mut Self,
1333                  secondary_confirm: bool,
1334                  window: &mut Window,
1335                  cx: &mut Context<Self>| {
1336                let Some(app_state) = remote_server_projects
1337                    .workspace
1338                    .read_with(cx, |workspace, _| workspace.app_state().clone())
1339                    .log_err()
1340                else {
1341                    return;
1342                };
1343                let project = project.clone();
1344                let server = server.connection().into_owned();
1345                cx.emit(DismissEvent);
1346
1347                let replace_window = match (create_new_window, secondary_confirm) {
1348                    (true, false) | (false, true) => None,
1349                    (true, true) | (false, false) => window.window_handle().downcast::<Workspace>(),
1350                };
1351
1352                cx.spawn_in(window, async move |_, cx| {
1353                    let result = open_remote_project(
1354                        server.into(),
1355                        project.paths.into_iter().map(PathBuf::from).collect(),
1356                        app_state,
1357                        OpenOptions {
1358                            replace_window,
1359                            ..OpenOptions::default()
1360                        },
1361                        cx,
1362                    )
1363                    .await;
1364                    if let Err(e) = result {
1365                        log::error!("Failed to connect: {e:#}");
1366                        cx.prompt(
1367                            gpui::PromptLevel::Critical,
1368                            "Failed to connect",
1369                            Some(&e.to_string()),
1370                            &["Ok"],
1371                        )
1372                        .await
1373                        .ok();
1374                    }
1375                })
1376                .detach();
1377            }
1378        });
1379
1380        div()
1381            .id((container_element_id_base, ix))
1382            .track_focus(&navigation.focus_handle)
1383            .anchor_scroll(navigation.scroll_anchor.clone())
1384            .on_action(cx.listener({
1385                let callback = callback.clone();
1386                move |this, _: &menu::Confirm, window, cx| {
1387                    callback(this, false, window, cx);
1388                }
1389            }))
1390            .on_action(cx.listener({
1391                let callback = callback.clone();
1392                move |this, _: &menu::SecondaryConfirm, window, cx| {
1393                    callback(this, true, window, cx);
1394                }
1395            }))
1396            .child(
1397                ListItem::new((element_id_base, ix))
1398                    .toggle_state(navigation.focus_handle.contains_focused(window, cx))
1399                    .inset(true)
1400                    .spacing(ui::ListItemSpacing::Sparse)
1401                    .start_slot(
1402                        Icon::new(IconName::Folder)
1403                            .color(Color::Muted)
1404                            .size(IconSize::Small),
1405                    )
1406                    .child(Label::new(project.paths.join(", ")))
1407                    .on_click(cx.listener(move |this, e: &ClickEvent, window, cx| {
1408                        let secondary_confirm = e.modifiers().platform;
1409                        callback(this, secondary_confirm, window, cx)
1410                    }))
1411                    .when(
1412                        is_from_zed && matches!(server_ix, ServerIndex::Ssh(_)),
1413                        |server_list_item| {
1414                            let ServerIndex::Ssh(server_ix) = server_ix else {
1415                                unreachable!()
1416                            };
1417                            server_list_item.end_hover_slot::<AnyElement>(Some(
1418                                div()
1419                                    .mr_2()
1420                                    .child({
1421                                        let project = project.clone();
1422                                        // Right-margin to offset it from the Scrollbar
1423                                        IconButton::new("remove-remote-project", IconName::Trash)
1424                                            .icon_size(IconSize::Small)
1425                                            .shape(IconButtonShape::Square)
1426                                            .size(ButtonSize::Large)
1427                                            .tooltip(Tooltip::text("Delete Remote Project"))
1428                                            .on_click(cx.listener(move |this, _, _, cx| {
1429                                                this.delete_ssh_project(server_ix, &project, cx)
1430                                            }))
1431                                    })
1432                                    .into_any_element(),
1433                            ))
1434                        },
1435                    ),
1436            )
1437    }
1438
1439    fn update_settings_file(
1440        &mut self,
1441        cx: &mut Context<Self>,
1442        f: impl FnOnce(&mut RemoteSettingsContent, &App) + Send + Sync + 'static,
1443    ) {
1444        let Some(fs) = self
1445            .workspace
1446            .read_with(cx, |workspace, _| workspace.app_state().fs.clone())
1447            .log_err()
1448        else {
1449            return;
1450        };
1451        update_settings_file::<SshSettings>(fs, cx, move |setting, cx| f(setting, cx));
1452    }
1453
1454    fn delete_ssh_server(&mut self, server: SshServerIndex, cx: &mut Context<Self>) {
1455        self.update_settings_file(cx, move |setting, _| {
1456            if let Some(connections) = setting.ssh_connections.as_mut() {
1457                connections.remove(server.0);
1458            }
1459        });
1460    }
1461
1462    fn delete_ssh_project(
1463        &mut self,
1464        server: SshServerIndex,
1465        project: &SshProject,
1466        cx: &mut Context<Self>,
1467    ) {
1468        let project = project.clone();
1469        self.update_settings_file(cx, move |setting, _| {
1470            if let Some(server) = setting
1471                .ssh_connections
1472                .as_mut()
1473                .and_then(|connections| connections.get_mut(server.0))
1474            {
1475                server.projects.remove(&project);
1476            }
1477        });
1478    }
1479
1480    #[cfg(target_os = "windows")]
1481    fn add_wsl_distro(
1482        &mut self,
1483        connection_options: remote::WslConnectionOptions,
1484        cx: &mut Context<Self>,
1485    ) {
1486        self.update_settings_file(cx, move |setting, _| {
1487            setting
1488                .wsl_connections
1489                .get_or_insert(Default::default())
1490                .push(crate::remote_connections::WslConnection {
1491                    distro_name: SharedString::from(connection_options.distro_name),
1492                    user: connection_options.user,
1493                    projects: BTreeSet::new(),
1494                })
1495        });
1496    }
1497
1498    fn delete_wsl_distro(&mut self, server: WslServerIndex, cx: &mut Context<Self>) {
1499        self.update_settings_file(cx, move |setting, _| {
1500            if let Some(connections) = setting.wsl_connections.as_mut() {
1501                connections.remove(server.0);
1502            }
1503        });
1504    }
1505
1506    fn add_ssh_server(
1507        &mut self,
1508        connection_options: remote::SshConnectionOptions,
1509        cx: &mut Context<Self>,
1510    ) {
1511        self.update_settings_file(cx, move |setting, _| {
1512            setting
1513                .ssh_connections
1514                .get_or_insert(Default::default())
1515                .push(SshConnection {
1516                    host: SharedString::from(connection_options.host),
1517                    username: connection_options.username,
1518                    port: connection_options.port,
1519                    projects: BTreeSet::new(),
1520                    nickname: None,
1521                    args: connection_options.args.unwrap_or_default(),
1522                    upload_binary_over_ssh: None,
1523                    port_forwards: connection_options.port_forwards,
1524                })
1525        });
1526    }
1527
1528    fn render_create_remote_server(
1529        &self,
1530        state: &CreateRemoteServer,
1531        window: &mut Window,
1532        cx: &mut Context<Self>,
1533    ) -> impl IntoElement {
1534        let ssh_prompt = state.ssh_prompt.clone();
1535
1536        state.address_editor.update(cx, |editor, cx| {
1537            if editor.text(cx).is_empty() {
1538                editor.set_placeholder_text("ssh user@example -p 2222", window, cx);
1539            }
1540        });
1541
1542        let theme = cx.theme();
1543
1544        v_flex()
1545            .track_focus(&self.focus_handle(cx))
1546            .id("create-remote-server")
1547            .overflow_hidden()
1548            .size_full()
1549            .flex_1()
1550            .child(
1551                div()
1552                    .p_2()
1553                    .border_b_1()
1554                    .border_color(theme.colors().border_variant)
1555                    .child(state.address_editor.clone()),
1556            )
1557            .child(
1558                h_flex()
1559                    .bg(theme.colors().editor_background)
1560                    .rounded_b_sm()
1561                    .w_full()
1562                    .map(|this| {
1563                        if let Some(ssh_prompt) = ssh_prompt {
1564                            this.child(h_flex().w_full().child(ssh_prompt))
1565                        } else if let Some(address_error) = &state.address_error {
1566                            this.child(
1567                                h_flex().p_2().w_full().gap_2().child(
1568                                    Label::new(address_error.clone())
1569                                        .size(LabelSize::Small)
1570                                        .color(Color::Error),
1571                                ),
1572                            )
1573                        } else {
1574                            this.child(
1575                                h_flex()
1576                                    .p_2()
1577                                    .w_full()
1578                                    .gap_1()
1579                                    .child(
1580                                        Label::new(
1581                                            "Enter the command you use to SSH into this server.",
1582                                        )
1583                                        .color(Color::Muted)
1584                                        .size(LabelSize::Small),
1585                                    )
1586                                    .child(
1587                                        Button::new("learn-more", "Learn More")
1588                                            .label_size(LabelSize::Small)
1589                                            .icon(IconName::ArrowUpRight)
1590                                            .icon_size(IconSize::XSmall)
1591                                            .on_click(|_, _, cx| {
1592                                                cx.open_url(
1593                                                    "https://zed.dev/docs/remote-development",
1594                                                );
1595                                            }),
1596                                    ),
1597                            )
1598                        }
1599                    }),
1600            )
1601    }
1602
1603    #[cfg(target_os = "windows")]
1604    fn render_add_wsl_distro(
1605        &self,
1606        state: &AddWslDistro,
1607        window: &mut Window,
1608        cx: &mut Context<Self>,
1609    ) -> impl IntoElement {
1610        let connection_prompt = state.connection_prompt.clone();
1611
1612        state.picker.update(cx, |picker, cx| {
1613            picker.focus_handle(cx).focus(window);
1614        });
1615
1616        v_flex()
1617            .id("add-wsl-distro")
1618            .overflow_hidden()
1619            .size_full()
1620            .flex_1()
1621            .map(|this| {
1622                if let Some(connection_prompt) = connection_prompt {
1623                    this.child(connection_prompt)
1624                } else {
1625                    this.child(state.picker.clone())
1626                }
1627            })
1628    }
1629
1630    fn render_view_options(
1631        &mut self,
1632        options: ViewServerOptionsState,
1633        window: &mut Window,
1634        cx: &mut Context<Self>,
1635    ) -> impl IntoElement {
1636        let last_entry = options.entries().last().unwrap();
1637
1638        let mut view = Navigable::new(
1639            div()
1640                .track_focus(&self.focus_handle(cx))
1641                .size_full()
1642                .child(match &options {
1643                    ViewServerOptionsState::Ssh { connection, .. } => SshConnectionHeader {
1644                        connection_string: connection.host.clone().into(),
1645                        paths: Default::default(),
1646                        nickname: connection.nickname.clone().map(|s| s.into()),
1647                    }
1648                    .render(window, cx)
1649                    .into_any_element(),
1650                    ViewServerOptionsState::Wsl { connection, .. } => SshConnectionHeader {
1651                        connection_string: connection.distro_name.clone().into(),
1652                        paths: Default::default(),
1653                        nickname: None,
1654                    }
1655                    .render(window, cx)
1656                    .into_any_element(),
1657                })
1658                .child(
1659                    v_flex()
1660                        .pb_1()
1661                        .child(ListSeparator)
1662                        .map(|this| match &options {
1663                            ViewServerOptionsState::Ssh {
1664                                connection,
1665                                entries,
1666                                server_index,
1667                            } => this.child(self.render_edit_ssh(
1668                                connection,
1669                                *server_index,
1670                                entries,
1671                                window,
1672                                cx,
1673                            )),
1674                            ViewServerOptionsState::Wsl {
1675                                connection,
1676                                entries,
1677                                server_index,
1678                            } => this.child(self.render_edit_wsl(
1679                                connection,
1680                                *server_index,
1681                                entries,
1682                                window,
1683                                cx,
1684                            )),
1685                        })
1686                        .child(ListSeparator)
1687                        .child({
1688                            div()
1689                                .id("ssh-options-copy-server-address")
1690                                .track_focus(&last_entry.focus_handle)
1691                                .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
1692                                    this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
1693                                    cx.focus_self(window);
1694                                    cx.notify();
1695                                }))
1696                                .child(
1697                                    ListItem::new("go-back")
1698                                        .toggle_state(
1699                                            last_entry.focus_handle.contains_focused(window, cx),
1700                                        )
1701                                        .inset(true)
1702                                        .spacing(ui::ListItemSpacing::Sparse)
1703                                        .start_slot(
1704                                            Icon::new(IconName::ArrowLeft).color(Color::Muted),
1705                                        )
1706                                        .child(Label::new("Go Back"))
1707                                        .on_click(cx.listener(|this, _, window, cx| {
1708                                            this.mode =
1709                                                Mode::default_mode(&this.ssh_config_servers, cx);
1710                                            cx.focus_self(window);
1711                                            cx.notify()
1712                                        })),
1713                                )
1714                        }),
1715                )
1716                .into_any_element(),
1717        );
1718
1719        for entry in options.entries() {
1720            view = view.entry(entry.clone());
1721        }
1722
1723        view.render(window, cx).into_any_element()
1724    }
1725
1726    fn render_edit_wsl(
1727        &self,
1728        connection: &WslConnectionOptions,
1729        index: WslServerIndex,
1730        entries: &[NavigableEntry],
1731        window: &mut Window,
1732        cx: &mut Context<Self>,
1733    ) -> impl IntoElement {
1734        let distro_name = SharedString::new(connection.distro_name.clone());
1735
1736        v_flex().child({
1737            fn remove_wsl_distro(
1738                remote_servers: Entity<RemoteServerProjects>,
1739                index: WslServerIndex,
1740                distro_name: SharedString,
1741                window: &mut Window,
1742                cx: &mut App,
1743            ) {
1744                let prompt_message = format!("Remove WSL distro `{}`?", distro_name);
1745
1746                let confirmation = window.prompt(
1747                    PromptLevel::Warning,
1748                    &prompt_message,
1749                    None,
1750                    &["Yes, remove it", "No, keep it"],
1751                    cx,
1752                );
1753
1754                cx.spawn(async move |cx| {
1755                    if confirmation.await.ok() == Some(0) {
1756                        remote_servers
1757                            .update(cx, |this, cx| {
1758                                this.delete_wsl_distro(index, cx);
1759                            })
1760                            .ok();
1761                        remote_servers
1762                            .update(cx, |this, cx| {
1763                                this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
1764                                cx.notify();
1765                            })
1766                            .ok();
1767                    }
1768                    anyhow::Ok(())
1769                })
1770                .detach_and_log_err(cx);
1771            }
1772            div()
1773                .id("wsl-options-remove-distro")
1774                .track_focus(&entries[0].focus_handle)
1775                .on_action(cx.listener({
1776                    let distro_name = distro_name.clone();
1777                    move |_, _: &menu::Confirm, window, cx| {
1778                        remove_wsl_distro(cx.entity(), index, distro_name.clone(), window, cx);
1779                        cx.focus_self(window);
1780                    }
1781                }))
1782                .child(
1783                    ListItem::new("remove-distro")
1784                        .toggle_state(entries[0].focus_handle.contains_focused(window, cx))
1785                        .inset(true)
1786                        .spacing(ui::ListItemSpacing::Sparse)
1787                        .start_slot(Icon::new(IconName::Trash).color(Color::Error))
1788                        .child(Label::new("Remove Distro").color(Color::Error))
1789                        .on_click(cx.listener(move |_, _, window, cx| {
1790                            remove_wsl_distro(cx.entity(), index, distro_name.clone(), window, cx);
1791                            cx.focus_self(window);
1792                        })),
1793                )
1794        })
1795    }
1796
1797    fn render_edit_ssh(
1798        &self,
1799        connection: &SshConnectionOptions,
1800        index: SshServerIndex,
1801        entries: &[NavigableEntry],
1802        window: &mut Window,
1803        cx: &mut Context<Self>,
1804    ) -> impl IntoElement {
1805        let connection_string = SharedString::new(connection.host.clone());
1806
1807        v_flex()
1808            .child({
1809                let label = if connection.nickname.is_some() {
1810                    "Edit Nickname"
1811                } else {
1812                    "Add Nickname to Server"
1813                };
1814                div()
1815                    .id("ssh-options-add-nickname")
1816                    .track_focus(&entries[0].focus_handle)
1817                    .on_action(cx.listener(move |this, _: &menu::Confirm, window, cx| {
1818                        this.mode = Mode::EditNickname(EditNicknameState::new(index, window, cx));
1819                        cx.notify();
1820                    }))
1821                    .child(
1822                        ListItem::new("add-nickname")
1823                            .toggle_state(entries[0].focus_handle.contains_focused(window, cx))
1824                            .inset(true)
1825                            .spacing(ui::ListItemSpacing::Sparse)
1826                            .start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
1827                            .child(Label::new(label))
1828                            .on_click(cx.listener(move |this, _, window, cx| {
1829                                this.mode =
1830                                    Mode::EditNickname(EditNicknameState::new(index, window, cx));
1831                                cx.notify();
1832                            })),
1833                    )
1834            })
1835            .child({
1836                let workspace = self.workspace.clone();
1837                fn callback(
1838                    workspace: WeakEntity<Workspace>,
1839                    connection_string: SharedString,
1840                    cx: &mut App,
1841                ) {
1842                    cx.write_to_clipboard(ClipboardItem::new_string(connection_string.to_string()));
1843                    workspace
1844                        .update(cx, |this, cx| {
1845                            struct SshServerAddressCopiedToClipboard;
1846                            let notification = format!(
1847                                "Copied server address ({}) to clipboard",
1848                                connection_string
1849                            );
1850
1851                            this.show_toast(
1852                                Toast::new(
1853                                    NotificationId::composite::<SshServerAddressCopiedToClipboard>(
1854                                        connection_string.clone(),
1855                                    ),
1856                                    notification,
1857                                )
1858                                .autohide(),
1859                                cx,
1860                            );
1861                        })
1862                        .ok();
1863                }
1864                div()
1865                    .id("ssh-options-copy-server-address")
1866                    .track_focus(&entries[1].focus_handle)
1867                    .on_action({
1868                        let connection_string = connection_string.clone();
1869                        let workspace = self.workspace.clone();
1870                        move |_: &menu::Confirm, _, cx| {
1871                            callback(workspace.clone(), connection_string.clone(), cx);
1872                        }
1873                    })
1874                    .child(
1875                        ListItem::new("copy-server-address")
1876                            .toggle_state(entries[1].focus_handle.contains_focused(window, cx))
1877                            .inset(true)
1878                            .spacing(ui::ListItemSpacing::Sparse)
1879                            .start_slot(Icon::new(IconName::Copy).color(Color::Muted))
1880                            .child(Label::new("Copy Server Address"))
1881                            .end_hover_slot(
1882                                Label::new(connection_string.clone()).color(Color::Muted),
1883                            )
1884                            .on_click({
1885                                let connection_string = connection_string.clone();
1886                                move |_, _, cx| {
1887                                    callback(workspace.clone(), connection_string.clone(), cx);
1888                                }
1889                            }),
1890                    )
1891            })
1892            .child({
1893                fn remove_ssh_server(
1894                    remote_servers: Entity<RemoteServerProjects>,
1895                    index: SshServerIndex,
1896                    connection_string: SharedString,
1897                    window: &mut Window,
1898                    cx: &mut App,
1899                ) {
1900                    let prompt_message = format!("Remove server `{}`?", connection_string);
1901
1902                    let confirmation = window.prompt(
1903                        PromptLevel::Warning,
1904                        &prompt_message,
1905                        None,
1906                        &["Yes, remove it", "No, keep it"],
1907                        cx,
1908                    );
1909
1910                    cx.spawn(async move |cx| {
1911                        if confirmation.await.ok() == Some(0) {
1912                            remote_servers
1913                                .update(cx, |this, cx| {
1914                                    this.delete_ssh_server(index, cx);
1915                                })
1916                                .ok();
1917                            remote_servers
1918                                .update(cx, |this, cx| {
1919                                    this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
1920                                    cx.notify();
1921                                })
1922                                .ok();
1923                        }
1924                        anyhow::Ok(())
1925                    })
1926                    .detach_and_log_err(cx);
1927                }
1928                div()
1929                    .id("ssh-options-copy-server-address")
1930                    .track_focus(&entries[2].focus_handle)
1931                    .on_action(cx.listener({
1932                        let connection_string = connection_string.clone();
1933                        move |_, _: &menu::Confirm, window, cx| {
1934                            remove_ssh_server(
1935                                cx.entity(),
1936                                index,
1937                                connection_string.clone(),
1938                                window,
1939                                cx,
1940                            );
1941                            cx.focus_self(window);
1942                        }
1943                    }))
1944                    .child(
1945                        ListItem::new("remove-server")
1946                            .toggle_state(entries[2].focus_handle.contains_focused(window, cx))
1947                            .inset(true)
1948                            .spacing(ui::ListItemSpacing::Sparse)
1949                            .start_slot(Icon::new(IconName::Trash).color(Color::Error))
1950                            .child(Label::new("Remove Server").color(Color::Error))
1951                            .on_click(cx.listener(move |_, _, window, cx| {
1952                                remove_ssh_server(
1953                                    cx.entity(),
1954                                    index,
1955                                    connection_string.clone(),
1956                                    window,
1957                                    cx,
1958                                );
1959                                cx.focus_self(window);
1960                            })),
1961                    )
1962            })
1963    }
1964
1965    fn render_edit_nickname(
1966        &self,
1967        state: &EditNicknameState,
1968        window: &mut Window,
1969        cx: &mut Context<Self>,
1970    ) -> impl IntoElement {
1971        let Some(connection) = SshSettings::get_global(cx)
1972            .ssh_connections()
1973            .nth(state.index.0)
1974        else {
1975            return v_flex()
1976                .id("ssh-edit-nickname")
1977                .track_focus(&self.focus_handle(cx));
1978        };
1979
1980        let connection_string = connection.host.clone();
1981        let nickname = connection.nickname.map(|s| s.into());
1982
1983        v_flex()
1984            .id("ssh-edit-nickname")
1985            .track_focus(&self.focus_handle(cx))
1986            .child(
1987                SshConnectionHeader {
1988                    connection_string,
1989                    paths: Default::default(),
1990                    nickname,
1991                }
1992                .render(window, cx),
1993            )
1994            .child(
1995                h_flex()
1996                    .p_2()
1997                    .border_t_1()
1998                    .border_color(cx.theme().colors().border_variant)
1999                    .child(state.editor.clone()),
2000            )
2001    }
2002
2003    fn render_default(
2004        &mut self,
2005        mut state: DefaultState,
2006        window: &mut Window,
2007        cx: &mut Context<Self>,
2008    ) -> impl IntoElement {
2009        let ssh_settings = SshSettings::get_global(cx);
2010        let mut should_rebuild = false;
2011
2012        let ssh_connections_changed =
2013            ssh_settings
2014                .ssh_connections
2015                .as_ref()
2016                .is_some_and(|connections| {
2017                    state
2018                        .servers
2019                        .iter()
2020                        .filter_map(|server| match server {
2021                            RemoteEntry::Project {
2022                                connection: Connection::Ssh(connection),
2023                                ..
2024                            } => Some(connection),
2025                            _ => None,
2026                        })
2027                        .ne(connections.iter())
2028                });
2029
2030        let wsl_connections_changed =
2031            ssh_settings
2032                .wsl_connections
2033                .as_ref()
2034                .is_some_and(|connections| {
2035                    state
2036                        .servers
2037                        .iter()
2038                        .filter_map(|server| match server {
2039                            RemoteEntry::Project {
2040                                connection: Connection::Wsl(connection),
2041                                ..
2042                            } => Some(connection),
2043                            _ => None,
2044                        })
2045                        .ne(connections.iter())
2046                });
2047
2048        if ssh_connections_changed || wsl_connections_changed {
2049            should_rebuild = true;
2050        };
2051
2052        if !should_rebuild && ssh_settings.read_ssh_config {
2053            let current_ssh_hosts: BTreeSet<SharedString> = state
2054                .servers
2055                .iter()
2056                .filter_map(|server| match server {
2057                    RemoteEntry::SshConfig { host, .. } => Some(host.clone()),
2058                    _ => None,
2059                })
2060                .collect();
2061            let mut expected_ssh_hosts = self.ssh_config_servers.clone();
2062            for server in &state.servers {
2063                if let RemoteEntry::Project {
2064                    connection: Connection::Ssh(connection),
2065                    ..
2066                } = server
2067                {
2068                    expected_ssh_hosts.remove(&connection.host);
2069                }
2070            }
2071            should_rebuild = current_ssh_hosts != expected_ssh_hosts;
2072        }
2073
2074        if should_rebuild {
2075            self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
2076            if let Mode::Default(new_state) = &self.mode {
2077                state = new_state.clone();
2078            }
2079        }
2080
2081        let connect_button = div()
2082            .id("ssh-connect-new-server-container")
2083            .track_focus(&state.add_new_server.focus_handle)
2084            .anchor_scroll(state.add_new_server.scroll_anchor.clone())
2085            .child(
2086                ListItem::new("register-remove-server-button")
2087                    .toggle_state(
2088                        state
2089                            .add_new_server
2090                            .focus_handle
2091                            .contains_focused(window, cx),
2092                    )
2093                    .inset(true)
2094                    .spacing(ui::ListItemSpacing::Sparse)
2095                    .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
2096                    .child(Label::new("Connect New Server"))
2097                    .on_click(cx.listener(|this, _, window, cx| {
2098                        let state = CreateRemoteServer::new(window, cx);
2099                        this.mode = Mode::CreateRemoteServer(state);
2100
2101                        cx.notify();
2102                    })),
2103            )
2104            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
2105                let state = CreateRemoteServer::new(window, cx);
2106                this.mode = Mode::CreateRemoteServer(state);
2107
2108                cx.notify();
2109            }));
2110
2111        #[cfg(target_os = "windows")]
2112        let wsl_connect_button = div()
2113            .id("wsl-connect-new-server")
2114            .track_focus(&state.add_new_wsl.focus_handle)
2115            .anchor_scroll(state.add_new_wsl.scroll_anchor.clone())
2116            .child(
2117                ListItem::new("wsl-add-new-server")
2118                    .toggle_state(state.add_new_wsl.focus_handle.contains_focused(window, cx))
2119                    .inset(true)
2120                    .spacing(ui::ListItemSpacing::Sparse)
2121                    .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
2122                    .child(Label::new("Add WSL Distro"))
2123                    .on_click(cx.listener(|this, _, window, cx| {
2124                        let state = AddWslDistro::new(window, cx);
2125                        this.mode = Mode::AddWslDistro(state);
2126
2127                        cx.notify();
2128                    })),
2129            )
2130            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
2131                let state = AddWslDistro::new(window, cx);
2132                this.mode = Mode::AddWslDistro(state);
2133
2134                cx.notify();
2135            }));
2136
2137        let modal_section = v_flex()
2138            .track_focus(&self.focus_handle(cx))
2139            .id("ssh-server-list")
2140            .overflow_y_scroll()
2141            .track_scroll(&state.scroll_handle)
2142            .size_full()
2143            .child(connect_button);
2144
2145        #[cfg(target_os = "windows")]
2146        let modal_section = modal_section.child(wsl_connect_button);
2147        #[cfg(not(target_os = "windows"))]
2148        let modal_section = modal_section;
2149
2150        let mut modal_section = Navigable::new(
2151            modal_section
2152                .child(
2153                    List::new()
2154                        .empty_message(
2155                            v_flex()
2156                                .child(
2157                                    div().px_3().child(
2158                                        Label::new("No remote servers registered yet.")
2159                                            .color(Color::Muted),
2160                                    ),
2161                                )
2162                                .into_any_element(),
2163                        )
2164                        .children(state.servers.iter().enumerate().map(|(ix, connection)| {
2165                            self.render_ssh_connection(ix, connection.clone(), window, cx)
2166                                .into_any_element()
2167                        })),
2168                )
2169                .into_any_element(),
2170        )
2171        .entry(state.add_new_server.clone())
2172        .entry(state.add_new_wsl.clone());
2173
2174        for server in &state.servers {
2175            match server {
2176                RemoteEntry::Project {
2177                    open_folder,
2178                    projects,
2179                    configure,
2180                    ..
2181                } => {
2182                    for (navigation_state, _) in projects {
2183                        modal_section = modal_section.entry(navigation_state.clone());
2184                    }
2185                    modal_section = modal_section
2186                        .entry(open_folder.clone())
2187                        .entry(configure.clone());
2188                }
2189                RemoteEntry::SshConfig { open_folder, .. } => {
2190                    modal_section = modal_section.entry(open_folder.clone());
2191                }
2192            }
2193        }
2194        let mut modal_section = modal_section.render(window, cx).into_any_element();
2195
2196        let (create_window, reuse_window) = if self.create_new_window {
2197            (
2198                window.keystroke_text_for(&menu::Confirm),
2199                window.keystroke_text_for(&menu::SecondaryConfirm),
2200            )
2201        } else {
2202            (
2203                window.keystroke_text_for(&menu::SecondaryConfirm),
2204                window.keystroke_text_for(&menu::Confirm),
2205            )
2206        };
2207        let placeholder_text = Arc::from(format!(
2208            "{reuse_window} reuses this window, {create_window} opens a new one",
2209        ));
2210
2211        Modal::new("remote-projects", None)
2212            .header(
2213                ModalHeader::new()
2214                    .child(Headline::new("Remote Projects").size(HeadlineSize::XSmall))
2215                    .child(
2216                        Label::new(placeholder_text)
2217                            .color(Color::Muted)
2218                            .size(LabelSize::XSmall),
2219                    ),
2220            )
2221            .section(
2222                Section::new().padded(false).child(
2223                    v_flex()
2224                        .min_h(rems(20.))
2225                        .size_full()
2226                        .relative()
2227                        .child(ListSeparator)
2228                        .child(
2229                            canvas(
2230                                |bounds, window, cx| {
2231                                    modal_section.prepaint_as_root(
2232                                        bounds.origin,
2233                                        bounds.size.into(),
2234                                        window,
2235                                        cx,
2236                                    );
2237                                    modal_section
2238                                },
2239                                |_, mut modal_section, window, cx| {
2240                                    modal_section.paint(window, cx);
2241                                },
2242                            )
2243                            .size_full(),
2244                        )
2245                        .vertical_scrollbar_for(state.scroll_handle, window, cx),
2246                ),
2247            )
2248            .into_any_element()
2249    }
2250
2251    fn create_host_from_ssh_config(
2252        &mut self,
2253        ssh_config_host: &SharedString,
2254        cx: &mut Context<'_, Self>,
2255    ) -> SshServerIndex {
2256        let new_ix = Arc::new(AtomicUsize::new(0));
2257
2258        let update_new_ix = new_ix.clone();
2259        self.update_settings_file(cx, move |settings, _| {
2260            update_new_ix.store(
2261                settings
2262                    .ssh_connections
2263                    .as_ref()
2264                    .map_or(0, |connections| connections.len()),
2265                atomic::Ordering::Release,
2266            );
2267        });
2268
2269        self.add_ssh_server(
2270            SshConnectionOptions {
2271                host: ssh_config_host.to_string(),
2272                ..SshConnectionOptions::default()
2273            },
2274            cx,
2275        );
2276        self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
2277        SshServerIndex(new_ix.load(atomic::Ordering::Acquire))
2278    }
2279}
2280
2281fn spawn_ssh_config_watch(fs: Arc<dyn Fs>, cx: &Context<RemoteServerProjects>) -> Task<()> {
2282    let mut user_ssh_config_watcher =
2283        watch_config_file(cx.background_executor(), fs.clone(), user_ssh_config_file());
2284    let mut global_ssh_config_watcher = watch_config_file(
2285        cx.background_executor(),
2286        fs,
2287        global_ssh_config_file().to_owned(),
2288    );
2289
2290    cx.spawn(async move |remote_server_projects, cx| {
2291        let mut global_hosts = BTreeSet::default();
2292        let mut user_hosts = BTreeSet::default();
2293        let mut running_receivers = 2;
2294
2295        loop {
2296            select! {
2297                new_global_file_contents = global_ssh_config_watcher.next().fuse() => {
2298                    match new_global_file_contents {
2299                        Some(new_global_file_contents) => {
2300                            global_hosts = parse_ssh_config_hosts(&new_global_file_contents);
2301                            if remote_server_projects.update(cx, |remote_server_projects, cx| {
2302                                remote_server_projects.ssh_config_servers = global_hosts.iter().chain(user_hosts.iter()).map(SharedString::from).collect();
2303                                cx.notify();
2304                            }).is_err() {
2305                                return;
2306                            }
2307                        },
2308                        None => {
2309                            running_receivers -= 1;
2310                            if running_receivers == 0 {
2311                                return;
2312                            }
2313                        }
2314                    }
2315                },
2316                new_user_file_contents = user_ssh_config_watcher.next().fuse() => {
2317                    match new_user_file_contents {
2318                        Some(new_user_file_contents) => {
2319                            user_hosts = parse_ssh_config_hosts(&new_user_file_contents);
2320                            if remote_server_projects.update(cx, |remote_server_projects, cx| {
2321                                remote_server_projects.ssh_config_servers = global_hosts.iter().chain(user_hosts.iter()).map(SharedString::from).collect();
2322                                cx.notify();
2323                            }).is_err() {
2324                                return;
2325                            }
2326                        },
2327                        None => {
2328                            running_receivers -= 1;
2329                            if running_receivers == 0 {
2330                                return;
2331                            }
2332                        }
2333                    }
2334                },
2335            }
2336        }
2337    })
2338}
2339
2340fn get_text(element: &Entity<Editor>, cx: &mut App) -> String {
2341    element.read(cx).text(cx).trim().to_string()
2342}
2343
2344impl ModalView for RemoteServerProjects {}
2345
2346impl Focusable for RemoteServerProjects {
2347    fn focus_handle(&self, cx: &App) -> FocusHandle {
2348        match &self.mode {
2349            Mode::ProjectPicker(picker) => picker.focus_handle(cx),
2350            _ => self.focus_handle.clone(),
2351        }
2352    }
2353}
2354
2355impl EventEmitter<DismissEvent> for RemoteServerProjects {}
2356
2357impl Render for RemoteServerProjects {
2358    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2359        div()
2360            .elevation_3(cx)
2361            .w(rems(34.))
2362            .key_context("RemoteServerModal")
2363            .on_action(cx.listener(Self::cancel))
2364            .on_action(cx.listener(Self::confirm))
2365            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
2366                this.focus_handle(cx).focus(window);
2367            }))
2368            .on_mouse_down_out(cx.listener(|this, _, _, cx| {
2369                if matches!(this.mode, Mode::Default(_)) {
2370                    cx.emit(DismissEvent)
2371                }
2372            }))
2373            .child(match &self.mode {
2374                Mode::Default(state) => self
2375                    .render_default(state.clone(), window, cx)
2376                    .into_any_element(),
2377                Mode::ViewServerOptions(state) => self
2378                    .render_view_options(state.clone(), window, cx)
2379                    .into_any_element(),
2380                Mode::ProjectPicker(element) => element.clone().into_any_element(),
2381                Mode::CreateRemoteServer(state) => self
2382                    .render_create_remote_server(state, window, cx)
2383                    .into_any_element(),
2384                Mode::EditNickname(state) => self
2385                    .render_edit_nickname(state, window, cx)
2386                    .into_any_element(),
2387                #[cfg(target_os = "windows")]
2388                Mode::AddWslDistro(state) => self
2389                    .render_add_wsl_distro(state, window, cx)
2390                    .into_any_element(),
2391            })
2392    }
2393}