remote_servers.rs

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