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