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