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