remote_servers.rs

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