remote_servers.rs

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