remote_servers.rs

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