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