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