remote_servers.rs

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