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