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                let prompt = workspace
1165                    .active_modal::<RemoteConnectionModal>(cx)
1166                    .unwrap()
1167                    .read(cx)
1168                    .prompt
1169                    .clone();
1170
1171                let connect = connect(
1172                    ConnectionIdentifier::setup(),
1173                    connection_options.clone(),
1174                    prompt,
1175                    window,
1176                    cx,
1177                )
1178                .prompt_err("Failed to connect", window, cx, |_, _, _| None);
1179
1180                cx.spawn_in(window, async move |workspace, cx| {
1181                    let session = connect.await;
1182
1183                    workspace.update(cx, |workspace, cx| {
1184                        if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) {
1185                            prompt.update(cx, |prompt, cx| prompt.finished(cx))
1186                        }
1187                    })?;
1188
1189                    let Some(Some(session)) = session else {
1190                        return workspace.update_in(cx, |workspace, window, cx| {
1191                            let weak = cx.entity().downgrade();
1192                            let fs = workspace.project().read(cx).fs().clone();
1193                            workspace.toggle_modal(window, cx, |window, cx| {
1194                                RemoteServerProjects::new(create_new_window, fs, window, weak, cx)
1195                            });
1196                        });
1197                    };
1198
1199                    let (path_style, project) = cx.update(|_, cx| {
1200                        (
1201                            session.read(cx).path_style(),
1202                            project::Project::remote(
1203                                session,
1204                                app_state.client.clone(),
1205                                app_state.node_runtime.clone(),
1206                                app_state.user_store.clone(),
1207                                app_state.languages.clone(),
1208                                app_state.fs.clone(),
1209                                true,
1210                                cx,
1211                            ),
1212                        )
1213                    })?;
1214
1215                    let home_dir = project
1216                        .read_with(cx, |project, cx| project.resolve_abs_path("~", cx))
1217                        .await
1218                        .and_then(|path| path.into_abs_path())
1219                        .map(|path| RemotePathBuf::new(path, path_style))
1220                        .unwrap_or_else(|| match path_style {
1221                            PathStyle::Posix => RemotePathBuf::from_str("/", PathStyle::Posix),
1222                            PathStyle::Windows => {
1223                                RemotePathBuf::from_str("C:\\", PathStyle::Windows)
1224                            }
1225                        });
1226
1227                    workspace
1228                        .update_in(cx, |workspace, window, cx| {
1229                            let weak = cx.entity().downgrade();
1230                            workspace.toggle_modal(window, cx, |window, cx| {
1231                                RemoteServerProjects::project_picker(
1232                                    create_new_window,
1233                                    index,
1234                                    connection_options,
1235                                    project,
1236                                    home_dir,
1237                                    window,
1238                                    cx,
1239                                    weak,
1240                                )
1241                            });
1242                        })
1243                        .ok();
1244                    Ok(())
1245                })
1246                .detach();
1247            })
1248        })
1249    }
1250
1251    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
1252        match &self.mode {
1253            Mode::Default(_) | Mode::ViewServerOptions(_) => {}
1254            Mode::ProjectPicker(_) => {}
1255            Mode::CreateRemoteServer(state) => {
1256                if let Some(prompt) = state.ssh_prompt.as_ref() {
1257                    prompt.update(cx, |prompt, cx| {
1258                        prompt.confirm(window, cx);
1259                    });
1260                    return;
1261                }
1262
1263                self.create_ssh_server(state.address_editor.clone(), window, cx);
1264            }
1265            Mode::CreateRemoteDevContainer(_) => {}
1266            Mode::EditNickname(state) => {
1267                let text = Some(state.editor.read(cx).text(cx)).filter(|text| !text.is_empty());
1268                let index = state.index;
1269                self.update_settings_file(cx, move |setting, _| {
1270                    if let Some(connections) = setting.ssh_connections.as_mut()
1271                        && let Some(connection) = connections.get_mut(index.0)
1272                    {
1273                        connection.nickname = text;
1274                    }
1275                });
1276                self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
1277                self.focus_handle.focus(window, cx);
1278            }
1279            #[cfg(target_os = "windows")]
1280            Mode::AddWslDistro(state) => {
1281                let delegate = &state.picker.read(cx).delegate;
1282                let distro = delegate.selected_distro().unwrap();
1283                self.connect_wsl_distro(state.picker.clone(), distro, window, cx);
1284            }
1285        }
1286    }
1287
1288    fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
1289        match &self.mode {
1290            Mode::Default(_) => cx.emit(DismissEvent),
1291            Mode::CreateRemoteServer(state) if state.ssh_prompt.is_some() => {
1292                let new_state = CreateRemoteServer::new(window, cx);
1293                let old_prompt = state.address_editor.read(cx).text(cx);
1294                new_state.address_editor.update(cx, |this, cx| {
1295                    this.set_text(old_prompt, window, cx);
1296                });
1297
1298                self.mode = Mode::CreateRemoteServer(new_state);
1299                cx.notify();
1300            }
1301            Mode::CreateRemoteDevContainer(CreateRemoteDevContainer {
1302                progress: DevContainerCreationProgress::Error(_),
1303                ..
1304            }) => {
1305                cx.emit(DismissEvent);
1306            }
1307            _ => {
1308                self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
1309                self.focus_handle(cx).focus(window, cx);
1310                cx.notify();
1311            }
1312        }
1313    }
1314
1315    fn render_remote_connection(
1316        &mut self,
1317        ix: usize,
1318        remote_server: RemoteEntry,
1319        window: &mut Window,
1320        cx: &mut Context<Self>,
1321    ) -> impl IntoElement {
1322        let connection = remote_server.connection().into_owned();
1323
1324        let (main_label, aux_label, is_wsl) = match &connection {
1325            Connection::Ssh(connection) => {
1326                if let Some(nickname) = connection.nickname.clone() {
1327                    let aux_label = SharedString::from(format!("({})", connection.host));
1328                    (nickname, Some(aux_label), false)
1329                } else {
1330                    (connection.host.clone(), None, false)
1331                }
1332            }
1333            Connection::Wsl(wsl_connection_options) => {
1334                (wsl_connection_options.distro_name.clone(), None, true)
1335            }
1336            Connection::DevContainer(dev_container_options) => {
1337                (dev_container_options.name.clone(), None, false)
1338            }
1339        };
1340        v_flex()
1341            .w_full()
1342            .child(ListSeparator)
1343            .child(
1344                h_flex()
1345                    .group("ssh-server")
1346                    .w_full()
1347                    .pt_0p5()
1348                    .px_3()
1349                    .gap_1()
1350                    .overflow_hidden()
1351                    .child(
1352                        h_flex()
1353                            .gap_1()
1354                            .max_w_96()
1355                            .overflow_hidden()
1356                            .text_ellipsis()
1357                            .when(is_wsl, |this| {
1358                                this.child(
1359                                    Label::new("WSL:")
1360                                        .size(LabelSize::Small)
1361                                        .color(Color::Muted),
1362                                )
1363                            })
1364                            .child(
1365                                Label::new(main_label)
1366                                    .size(LabelSize::Small)
1367                                    .color(Color::Muted),
1368                            ),
1369                    )
1370                    .children(
1371                        aux_label.map(|label| {
1372                            Label::new(label).size(LabelSize::Small).color(Color::Muted)
1373                        }),
1374                    ),
1375            )
1376            .child(match &remote_server {
1377                RemoteEntry::Project {
1378                    open_folder,
1379                    projects,
1380                    configure,
1381                    connection,
1382                    index,
1383                } => {
1384                    let index = *index;
1385                    List::new()
1386                        .empty_message("No projects.")
1387                        .children(projects.iter().enumerate().map(|(pix, p)| {
1388                            v_flex().gap_0p5().child(self.render_remote_project(
1389                                index,
1390                                remote_server.clone(),
1391                                pix,
1392                                p,
1393                                window,
1394                                cx,
1395                            ))
1396                        }))
1397                        .child(
1398                            h_flex()
1399                                .id(("new-remote-project-container", ix))
1400                                .track_focus(&open_folder.focus_handle)
1401                                .anchor_scroll(open_folder.scroll_anchor.clone())
1402                                .on_action(cx.listener({
1403                                    let connection = connection.clone();
1404                                    move |this, _: &menu::Confirm, window, cx| {
1405                                        this.create_remote_project(
1406                                            index,
1407                                            connection.clone().into(),
1408                                            window,
1409                                            cx,
1410                                        );
1411                                    }
1412                                }))
1413                                .child(
1414                                    ListItem::new(("new-remote-project", ix))
1415                                        .toggle_state(
1416                                            open_folder.focus_handle.contains_focused(window, cx),
1417                                        )
1418                                        .inset(true)
1419                                        .spacing(ui::ListItemSpacing::Sparse)
1420                                        .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
1421                                        .child(Label::new("Open Folder"))
1422                                        .on_click(cx.listener({
1423                                            let connection = connection.clone();
1424                                            move |this, _, window, cx| {
1425                                                this.create_remote_project(
1426                                                    index,
1427                                                    connection.clone().into(),
1428                                                    window,
1429                                                    cx,
1430                                                );
1431                                            }
1432                                        })),
1433                                ),
1434                        )
1435                        .child(
1436                            h_flex()
1437                                .id(("server-options-container", ix))
1438                                .track_focus(&configure.focus_handle)
1439                                .anchor_scroll(configure.scroll_anchor.clone())
1440                                .on_action(cx.listener({
1441                                    let connection = connection.clone();
1442                                    move |this, _: &menu::Confirm, window, cx| {
1443                                        this.view_server_options(
1444                                            (index, connection.clone().into()),
1445                                            window,
1446                                            cx,
1447                                        );
1448                                    }
1449                                }))
1450                                .child(
1451                                    ListItem::new(("server-options", ix))
1452                                        .toggle_state(
1453                                            configure.focus_handle.contains_focused(window, cx),
1454                                        )
1455                                        .inset(true)
1456                                        .spacing(ui::ListItemSpacing::Sparse)
1457                                        .start_slot(
1458                                            Icon::new(IconName::Settings).color(Color::Muted),
1459                                        )
1460                                        .child(Label::new("View Server Options"))
1461                                        .on_click(cx.listener({
1462                                            let ssh_connection = connection.clone();
1463                                            move |this, _, window, cx| {
1464                                                this.view_server_options(
1465                                                    (index, ssh_connection.clone().into()),
1466                                                    window,
1467                                                    cx,
1468                                                );
1469                                            }
1470                                        })),
1471                                ),
1472                        )
1473                }
1474                RemoteEntry::SshConfig { open_folder, host } => List::new().child(
1475                    h_flex()
1476                        .id(("new-remote-project-container", ix))
1477                        .track_focus(&open_folder.focus_handle)
1478                        .anchor_scroll(open_folder.scroll_anchor.clone())
1479                        .on_action(cx.listener({
1480                            let connection = connection.clone();
1481                            let host = host.clone();
1482                            move |this, _: &menu::Confirm, window, cx| {
1483                                let new_ix = this.create_host_from_ssh_config(&host, cx);
1484                                this.create_remote_project(
1485                                    new_ix.into(),
1486                                    connection.clone().into(),
1487                                    window,
1488                                    cx,
1489                                );
1490                            }
1491                        }))
1492                        .child(
1493                            ListItem::new(("new-remote-project", ix))
1494                                .toggle_state(open_folder.focus_handle.contains_focused(window, cx))
1495                                .inset(true)
1496                                .spacing(ui::ListItemSpacing::Sparse)
1497                                .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
1498                                .child(Label::new("Open Folder"))
1499                                .on_click(cx.listener({
1500                                    let host = host.clone();
1501                                    move |this, _, window, cx| {
1502                                        let new_ix = this.create_host_from_ssh_config(&host, cx);
1503                                        this.create_remote_project(
1504                                            new_ix.into(),
1505                                            connection.clone().into(),
1506                                            window,
1507                                            cx,
1508                                        );
1509                                    }
1510                                })),
1511                        ),
1512                ),
1513            })
1514    }
1515
1516    fn render_remote_project(
1517        &mut self,
1518        server_ix: ServerIndex,
1519        server: RemoteEntry,
1520        ix: usize,
1521        (navigation, project): &(NavigableEntry, RemoteProject),
1522        window: &mut Window,
1523        cx: &mut Context<Self>,
1524    ) -> impl IntoElement {
1525        let create_new_window = self.create_new_window;
1526        let is_from_zed = server.is_from_zed();
1527        let element_id_base = SharedString::from(format!(
1528            "remote-project-{}",
1529            match server_ix {
1530                ServerIndex::Ssh(index) => format!("ssh-{index}"),
1531                ServerIndex::Wsl(index) => format!("wsl-{index}"),
1532            }
1533        ));
1534        let container_element_id_base =
1535            SharedString::from(format!("remote-project-container-{element_id_base}"));
1536
1537        let callback = Rc::new({
1538            let project = project.clone();
1539            move |remote_server_projects: &mut Self,
1540                  secondary_confirm: bool,
1541                  window: &mut Window,
1542                  cx: &mut Context<Self>| {
1543                let Some(app_state) = remote_server_projects
1544                    .workspace
1545                    .read_with(cx, |workspace, _| workspace.app_state().clone())
1546                    .log_err()
1547                else {
1548                    return;
1549                };
1550                let project = project.clone();
1551                let server = server.connection().into_owned();
1552                cx.emit(DismissEvent);
1553
1554                let replace_window = match (create_new_window, secondary_confirm) {
1555                    (true, false) | (false, true) => None,
1556                    (true, true) | (false, false) => {
1557                        window.window_handle().downcast::<MultiWorkspace>()
1558                    }
1559                };
1560
1561                cx.spawn_in(window, async move |_, cx| {
1562                    let result = open_remote_project(
1563                        server.into(),
1564                        project.paths.into_iter().map(PathBuf::from).collect(),
1565                        app_state,
1566                        OpenOptions {
1567                            replace_window,
1568                            ..OpenOptions::default()
1569                        },
1570                        cx,
1571                    )
1572                    .await;
1573                    if let Err(e) = result {
1574                        log::error!("Failed to connect: {e:#}");
1575                        cx.prompt(
1576                            gpui::PromptLevel::Critical,
1577                            "Failed to connect",
1578                            Some(&e.to_string()),
1579                            &["Ok"],
1580                        )
1581                        .await
1582                        .ok();
1583                    }
1584                })
1585                .detach();
1586            }
1587        });
1588
1589        div()
1590            .id((container_element_id_base, ix))
1591            .track_focus(&navigation.focus_handle)
1592            .anchor_scroll(navigation.scroll_anchor.clone())
1593            .on_action(cx.listener({
1594                let callback = callback.clone();
1595                move |this, _: &menu::Confirm, window, cx| {
1596                    callback(this, false, window, cx);
1597                }
1598            }))
1599            .on_action(cx.listener({
1600                let callback = callback.clone();
1601                move |this, _: &menu::SecondaryConfirm, window, cx| {
1602                    callback(this, true, window, cx);
1603                }
1604            }))
1605            .child(
1606                ListItem::new((element_id_base, ix))
1607                    .toggle_state(navigation.focus_handle.contains_focused(window, cx))
1608                    .inset(true)
1609                    .spacing(ui::ListItemSpacing::Sparse)
1610                    .start_slot(
1611                        Icon::new(IconName::Folder)
1612                            .color(Color::Muted)
1613                            .size(IconSize::Small),
1614                    )
1615                    .child(Label::new(project.paths.join(", ")).truncate_start())
1616                    .on_click(cx.listener(move |this, e: &ClickEvent, window, cx| {
1617                        let secondary_confirm = e.modifiers().platform;
1618                        callback(this, secondary_confirm, window, cx)
1619                    }))
1620                    .tooltip(Tooltip::text(project.paths.join("\n")))
1621                    .when(is_from_zed, |server_list_item| {
1622                        server_list_item.end_hover_slot::<AnyElement>(Some(
1623                            div()
1624                                .mr_2()
1625                                .child({
1626                                    let project = project.clone();
1627                                    // Right-margin to offset it from the Scrollbar
1628                                    IconButton::new("remove-remote-project", IconName::Trash)
1629                                        .icon_size(IconSize::Small)
1630                                        .shape(IconButtonShape::Square)
1631                                        .size(ButtonSize::Large)
1632                                        .tooltip(Tooltip::text("Delete Remote Project"))
1633                                        .on_click(cx.listener(move |this, _, _, cx| {
1634                                            this.delete_remote_project(server_ix, &project, cx)
1635                                        }))
1636                                })
1637                                .into_any_element(),
1638                        ))
1639                    }),
1640            )
1641    }
1642
1643    fn update_settings_file(
1644        &mut self,
1645        cx: &mut Context<Self>,
1646        f: impl FnOnce(&mut RemoteSettingsContent, &App) + Send + Sync + 'static,
1647    ) {
1648        let Some(fs) = self
1649            .workspace
1650            .read_with(cx, |workspace, _| workspace.app_state().fs.clone())
1651            .log_err()
1652        else {
1653            return;
1654        };
1655        update_settings_file(fs, cx, move |setting, cx| f(&mut setting.remote, cx));
1656    }
1657
1658    fn delete_ssh_server(&mut self, server: SshServerIndex, cx: &mut Context<Self>) {
1659        self.update_settings_file(cx, move |setting, _| {
1660            if let Some(connections) = setting.ssh_connections.as_mut() {
1661                connections.remove(server.0);
1662            }
1663        });
1664    }
1665
1666    fn delete_remote_project(
1667        &mut self,
1668        server: ServerIndex,
1669        project: &RemoteProject,
1670        cx: &mut Context<Self>,
1671    ) {
1672        match server {
1673            ServerIndex::Ssh(server) => {
1674                self.delete_ssh_project(server, project, cx);
1675            }
1676            ServerIndex::Wsl(server) => {
1677                self.delete_wsl_project(server, project, cx);
1678            }
1679        }
1680    }
1681
1682    fn delete_ssh_project(
1683        &mut self,
1684        server: SshServerIndex,
1685        project: &RemoteProject,
1686        cx: &mut Context<Self>,
1687    ) {
1688        let project = project.clone();
1689        self.update_settings_file(cx, move |setting, _| {
1690            if let Some(server) = setting
1691                .ssh_connections
1692                .as_mut()
1693                .and_then(|connections| connections.get_mut(server.0))
1694            {
1695                server.projects.remove(&project);
1696            }
1697        });
1698    }
1699
1700    fn delete_wsl_project(
1701        &mut self,
1702        server: WslServerIndex,
1703        project: &RemoteProject,
1704        cx: &mut Context<Self>,
1705    ) {
1706        let project = project.clone();
1707        self.update_settings_file(cx, move |setting, _| {
1708            if let Some(server) = setting
1709                .wsl_connections
1710                .as_mut()
1711                .and_then(|connections| connections.get_mut(server.0))
1712            {
1713                server.projects.remove(&project);
1714            }
1715        });
1716    }
1717
1718    fn delete_wsl_distro(&mut self, server: WslServerIndex, cx: &mut Context<Self>) {
1719        self.update_settings_file(cx, move |setting, _| {
1720            if let Some(connections) = setting.wsl_connections.as_mut() {
1721                connections.remove(server.0);
1722            }
1723        });
1724    }
1725
1726    fn add_ssh_server(
1727        &mut self,
1728        connection_options: remote::SshConnectionOptions,
1729        cx: &mut Context<Self>,
1730    ) {
1731        self.update_settings_file(cx, move |setting, _| {
1732            setting
1733                .ssh_connections
1734                .get_or_insert(Default::default())
1735                .push(SshConnection {
1736                    host: connection_options.host.to_string(),
1737                    username: connection_options.username,
1738                    port: connection_options.port,
1739                    projects: BTreeSet::new(),
1740                    nickname: None,
1741                    args: connection_options.args.unwrap_or_default(),
1742                    upload_binary_over_ssh: None,
1743                    port_forwards: connection_options.port_forwards,
1744                    connection_timeout: connection_options.connection_timeout,
1745                })
1746        });
1747    }
1748
1749    fn edit_in_dev_container_json(
1750        &mut self,
1751        config: Option<DevContainerConfig>,
1752        window: &mut Window,
1753        cx: &mut Context<Self>,
1754    ) {
1755        let Some(workspace) = self.workspace.upgrade() else {
1756            cx.emit(DismissEvent);
1757            cx.notify();
1758            return;
1759        };
1760
1761        let config_path = config
1762            .map(|c| c.config_path)
1763            .unwrap_or_else(|| PathBuf::from(".devcontainer/devcontainer.json"));
1764
1765        workspace.update(cx, |workspace, cx| {
1766            let project = workspace.project().clone();
1767
1768            let worktree = project
1769                .read(cx)
1770                .visible_worktrees(cx)
1771                .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
1772
1773            if let Some(worktree) = worktree {
1774                let tree_id = worktree.read(cx).id();
1775                let devcontainer_path =
1776                    match RelPath::new(&config_path, util::paths::PathStyle::Posix) {
1777                        Ok(path) => path.into_owned(),
1778                        Err(error) => {
1779                            log::error!(
1780                                "Invalid devcontainer path: {} - {}",
1781                                config_path.display(),
1782                                error
1783                            );
1784                            return;
1785                        }
1786                    };
1787                cx.spawn_in(window, async move |workspace, cx| {
1788                    workspace
1789                        .update_in(cx, |workspace, window, cx| {
1790                            workspace.open_path(
1791                                (tree_id, devcontainer_path),
1792                                None,
1793                                true,
1794                                window,
1795                                cx,
1796                            )
1797                        })?
1798                        .await
1799                })
1800                .detach();
1801            } else {
1802                return;
1803            }
1804        });
1805        cx.emit(DismissEvent);
1806        cx.notify();
1807    }
1808
1809    fn init_dev_container_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1810        let configs = self
1811            .workspace
1812            .read_with(cx, |workspace, cx| find_devcontainer_configs(workspace, cx))
1813            .unwrap_or_default();
1814
1815        if configs.len() > 1 {
1816            let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
1817            self.dev_container_picker =
1818                Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)));
1819
1820            let state =
1821                CreateRemoteDevContainer::new(DevContainerCreationProgress::SelectingConfig, cx);
1822            self.mode = Mode::CreateRemoteDevContainer(state);
1823            cx.notify();
1824        } else if let Some((app_state, context)) = self
1825            .workspace
1826            .read_with(cx, |workspace, cx| {
1827                let app_state = workspace.app_state().clone();
1828                let context = DevContainerContext::from_workspace(workspace, cx)?;
1829                Some((app_state, context))
1830            })
1831            .ok()
1832            .flatten()
1833        {
1834            let config = configs.into_iter().next();
1835            self.open_dev_container(config, app_state, context, window, cx);
1836            self.view_in_progress_dev_container(window, cx);
1837        } else {
1838            log::error!("No active project directory for Dev Container");
1839        }
1840    }
1841
1842    fn open_dev_container(
1843        &self,
1844        config: Option<DevContainerConfig>,
1845        app_state: Arc<AppState>,
1846        context: DevContainerContext,
1847        window: &mut Window,
1848        cx: &mut Context<Self>,
1849    ) {
1850        let replace_window = window.window_handle().downcast::<MultiWorkspace>();
1851
1852        cx.spawn_in(window, async move |entity, cx| {
1853            let (connection, starting_dir) =
1854                match start_dev_container_with_config(context, config).await {
1855                    Ok((c, s)) => (Connection::DevContainer(c), s),
1856                    Err(e) => {
1857                        log::error!("Failed to start dev container: {:?}", e);
1858                        cx.prompt(
1859                            gpui::PromptLevel::Critical,
1860                            "Failed to start Dev Container. See logs for details",
1861                            Some(&format!("{e}")),
1862                            &["Ok"],
1863                        )
1864                        .await
1865                        .ok();
1866                        entity
1867                            .update_in(cx, |remote_server_projects, window, cx| {
1868                                remote_server_projects.mode =
1869                                    Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(
1870                                        DevContainerCreationProgress::Error(format!("{e}")),
1871                                        cx,
1872                                    ));
1873                                remote_server_projects.focus_handle(cx).focus(window, cx);
1874                            })
1875                            .ok();
1876                        return;
1877                    }
1878                };
1879            entity
1880                .update(cx, |_, cx| {
1881                    cx.emit(DismissEvent);
1882                })
1883                .log_err();
1884
1885            let result = open_remote_project(
1886                connection.into(),
1887                vec![starting_dir].into_iter().map(PathBuf::from).collect(),
1888                app_state,
1889                OpenOptions {
1890                    replace_window,
1891                    ..OpenOptions::default()
1892                },
1893                cx,
1894            )
1895            .await;
1896            if let Err(e) = result {
1897                log::error!("Failed to connect: {e:#}");
1898                cx.prompt(
1899                    gpui::PromptLevel::Critical,
1900                    "Failed to connect",
1901                    Some(&e.to_string()),
1902                    &["Ok"],
1903                )
1904                .await
1905                .ok();
1906            }
1907        })
1908        .detach();
1909    }
1910
1911    fn render_create_dev_container(
1912        &self,
1913        state: &CreateRemoteDevContainer,
1914        window: &mut Window,
1915        cx: &mut Context<Self>,
1916    ) -> impl IntoElement {
1917        match &state.progress {
1918            DevContainerCreationProgress::Error(message) => {
1919                let view = Navigable::new(
1920                    div()
1921                        .child(
1922                            div().track_focus(&self.focus_handle(cx)).size_full().child(
1923                                v_flex().py_1().child(
1924                                    ListItem::new("Error")
1925                                        .inset(true)
1926                                        .selectable(false)
1927                                        .spacing(ui::ListItemSpacing::Sparse)
1928                                        .start_slot(
1929                                            Icon::new(IconName::XCircle).color(Color::Error),
1930                                        )
1931                                        .child(Label::new("Error Creating Dev Container:"))
1932                                        .child(Label::new(message).buffer_font(cx)),
1933                                ),
1934                            ),
1935                        )
1936                        .child(ListSeparator)
1937                        .child(
1938                            div()
1939                                .id("devcontainer-see-log")
1940                                .track_focus(&state.view_logs_entry.focus_handle)
1941                                .on_action(cx.listener(|_, _: &menu::Confirm, window, cx| {
1942                                    window.dispatch_action(Box::new(OpenLog), cx);
1943                                    cx.emit(DismissEvent);
1944                                    cx.notify();
1945                                }))
1946                                .child(
1947                                    ListItem::new("li-devcontainer-see-log")
1948                                        .toggle_state(
1949                                            state
1950                                                .view_logs_entry
1951                                                .focus_handle
1952                                                .contains_focused(window, cx),
1953                                        )
1954                                        .inset(true)
1955                                        .spacing(ui::ListItemSpacing::Sparse)
1956                                        .start_slot(Icon::new(IconName::File).color(Color::Muted))
1957                                        .child(Label::new("Open Zed Log"))
1958                                        .on_click(cx.listener(|_, _, window, cx| {
1959                                            window.dispatch_action(Box::new(OpenLog), cx);
1960                                            cx.emit(DismissEvent);
1961                                            cx.notify();
1962                                        })),
1963                                ),
1964                        )
1965                        .child(
1966                            div()
1967                                .id("devcontainer-go-back")
1968                                .track_focus(&state.back_entry.focus_handle)
1969                                .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
1970                                    this.cancel(&menu::Cancel, window, cx);
1971                                    cx.notify();
1972                                }))
1973                                .child(
1974                                    ListItem::new("li-devcontainer-go-back")
1975                                        .toggle_state(
1976                                            state
1977                                                .back_entry
1978                                                .focus_handle
1979                                                .contains_focused(window, cx),
1980                                        )
1981                                        .inset(true)
1982                                        .spacing(ui::ListItemSpacing::Sparse)
1983                                        .start_slot(Icon::new(IconName::Exit).color(Color::Muted))
1984                                        .child(Label::new("Exit"))
1985                                        .on_click(cx.listener(|this, _, window, cx| {
1986                                            this.cancel(&menu::Cancel, window, cx);
1987                                            cx.notify();
1988                                        })),
1989                                ),
1990                        )
1991                        .into_any_element(),
1992                )
1993                .entry(state.view_logs_entry.clone())
1994                .entry(state.back_entry.clone());
1995                view.render(window, cx).into_any_element()
1996            }
1997            DevContainerCreationProgress::SelectingConfig => {
1998                self.render_config_selection(window, cx).into_any_element()
1999            }
2000            DevContainerCreationProgress::Creating => {
2001                self.focus_handle(cx).focus(window, cx);
2002                div()
2003                    .track_focus(&self.focus_handle(cx))
2004                    .size_full()
2005                    .child(
2006                        v_flex()
2007                            .pb_1()
2008                            .child(
2009                                ModalHeader::new().child(
2010                                    Headline::new("Dev Containers").size(HeadlineSize::XSmall),
2011                                ),
2012                            )
2013                            .child(ListSeparator)
2014                            .child(
2015                                ListItem::new("creating")
2016                                    .inset(true)
2017                                    .spacing(ui::ListItemSpacing::Sparse)
2018                                    .disabled(true)
2019                                    .start_slot(
2020                                        Icon::new(IconName::ArrowCircle)
2021                                            .color(Color::Muted)
2022                                            .with_rotate_animation(2),
2023                                    )
2024                                    .child(
2025                                        h_flex()
2026                                            .opacity(0.6)
2027                                            .gap_1()
2028                                            .child(Label::new("Creating Dev Container"))
2029                                            .child(LoadingLabel::new("")),
2030                                    ),
2031                            ),
2032                    )
2033                    .into_any_element()
2034            }
2035        }
2036    }
2037
2038    fn render_config_selection(
2039        &self,
2040        window: &mut Window,
2041        cx: &mut Context<Self>,
2042    ) -> impl IntoElement {
2043        let Some(picker) = &self.dev_container_picker else {
2044            return div().into_any_element();
2045        };
2046
2047        let content = v_flex().pb_1().child(picker.clone().into_any_element());
2048
2049        picker.focus_handle(cx).focus(window, cx);
2050
2051        content.into_any_element()
2052    }
2053
2054    fn render_create_remote_server(
2055        &self,
2056        state: &CreateRemoteServer,
2057        window: &mut Window,
2058        cx: &mut Context<Self>,
2059    ) -> impl IntoElement {
2060        let ssh_prompt = state.ssh_prompt.clone();
2061
2062        state.address_editor.update(cx, |editor, cx| {
2063            if editor.text(cx).is_empty() {
2064                editor.set_placeholder_text("ssh user@example -p 2222", window, cx);
2065            }
2066        });
2067
2068        let theme = cx.theme();
2069
2070        v_flex()
2071            .track_focus(&self.focus_handle(cx))
2072            .id("create-remote-server")
2073            .overflow_hidden()
2074            .size_full()
2075            .flex_1()
2076            .child(
2077                div()
2078                    .p_2()
2079                    .border_b_1()
2080                    .border_color(theme.colors().border_variant)
2081                    .child(state.address_editor.clone()),
2082            )
2083            .child(
2084                h_flex()
2085                    .bg(theme.colors().editor_background)
2086                    .rounded_b_sm()
2087                    .w_full()
2088                    .map(|this| {
2089                        if let Some(ssh_prompt) = ssh_prompt {
2090                            this.child(h_flex().w_full().child(ssh_prompt))
2091                        } else if let Some(address_error) = &state.address_error {
2092                            this.child(
2093                                h_flex().p_2().w_full().gap_2().child(
2094                                    Label::new(address_error.clone())
2095                                        .size(LabelSize::Small)
2096                                        .color(Color::Error),
2097                                ),
2098                            )
2099                        } else {
2100                            this.child(
2101                                h_flex()
2102                                    .p_2()
2103                                    .w_full()
2104                                    .gap_1()
2105                                    .child(
2106                                        Label::new(
2107                                            "Enter the command you use to SSH into this server.",
2108                                        )
2109                                        .color(Color::Muted)
2110                                        .size(LabelSize::Small),
2111                                    )
2112                                    .child(
2113                                        Button::new("learn-more", "Learn More")
2114                                            .label_size(LabelSize::Small)
2115                                            .icon(IconName::ArrowUpRight)
2116                                            .icon_size(IconSize::XSmall)
2117                                            .on_click(|_, _, cx| {
2118                                                cx.open_url(
2119                                                    "https://zed.dev/docs/remote-development",
2120                                                );
2121                                            }),
2122                                    ),
2123                            )
2124                        }
2125                    }),
2126            )
2127    }
2128
2129    #[cfg(target_os = "windows")]
2130    fn render_add_wsl_distro(
2131        &self,
2132        state: &AddWslDistro,
2133        window: &mut Window,
2134        cx: &mut Context<Self>,
2135    ) -> impl IntoElement {
2136        let connection_prompt = state.connection_prompt.clone();
2137
2138        state.picker.update(cx, |picker, cx| {
2139            picker.focus_handle(cx).focus(window, cx);
2140        });
2141
2142        v_flex()
2143            .id("add-wsl-distro")
2144            .overflow_hidden()
2145            .size_full()
2146            .flex_1()
2147            .map(|this| {
2148                if let Some(connection_prompt) = connection_prompt {
2149                    this.child(connection_prompt)
2150                } else {
2151                    this.child(state.picker.clone())
2152                }
2153            })
2154    }
2155
2156    fn render_view_options(
2157        &mut self,
2158        options: ViewServerOptionsState,
2159        window: &mut Window,
2160        cx: &mut Context<Self>,
2161    ) -> impl IntoElement {
2162        let last_entry = options.entries().last().unwrap();
2163
2164        let mut view = Navigable::new(
2165            div()
2166                .track_focus(&self.focus_handle(cx))
2167                .size_full()
2168                .child(match &options {
2169                    ViewServerOptionsState::Ssh { connection, .. } => SshConnectionHeader {
2170                        connection_string: connection.host.to_string().into(),
2171                        paths: Default::default(),
2172                        nickname: connection.nickname.clone().map(|s| s.into()),
2173                        is_wsl: false,
2174                        is_devcontainer: false,
2175                    }
2176                    .render(window, cx)
2177                    .into_any_element(),
2178                    ViewServerOptionsState::Wsl { connection, .. } => SshConnectionHeader {
2179                        connection_string: connection.distro_name.clone().into(),
2180                        paths: Default::default(),
2181                        nickname: None,
2182                        is_wsl: true,
2183                        is_devcontainer: false,
2184                    }
2185                    .render(window, cx)
2186                    .into_any_element(),
2187                })
2188                .child(
2189                    v_flex()
2190                        .pb_1()
2191                        .child(ListSeparator)
2192                        .map(|this| match &options {
2193                            ViewServerOptionsState::Ssh {
2194                                connection,
2195                                entries,
2196                                server_index,
2197                            } => this.child(self.render_edit_ssh(
2198                                connection,
2199                                *server_index,
2200                                entries,
2201                                window,
2202                                cx,
2203                            )),
2204                            ViewServerOptionsState::Wsl {
2205                                connection,
2206                                entries,
2207                                server_index,
2208                            } => this.child(self.render_edit_wsl(
2209                                connection,
2210                                *server_index,
2211                                entries,
2212                                window,
2213                                cx,
2214                            )),
2215                        })
2216                        .child(ListSeparator)
2217                        .child({
2218                            div()
2219                                .id("ssh-options-copy-server-address")
2220                                .track_focus(&last_entry.focus_handle)
2221                                .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
2222                                    this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
2223                                    cx.focus_self(window);
2224                                    cx.notify();
2225                                }))
2226                                .child(
2227                                    ListItem::new("go-back")
2228                                        .toggle_state(
2229                                            last_entry.focus_handle.contains_focused(window, cx),
2230                                        )
2231                                        .inset(true)
2232                                        .spacing(ui::ListItemSpacing::Sparse)
2233                                        .start_slot(
2234                                            Icon::new(IconName::ArrowLeft).color(Color::Muted),
2235                                        )
2236                                        .child(Label::new("Go Back"))
2237                                        .on_click(cx.listener(|this, _, window, cx| {
2238                                            this.mode =
2239                                                Mode::default_mode(&this.ssh_config_servers, cx);
2240                                            cx.focus_self(window);
2241                                            cx.notify()
2242                                        })),
2243                                )
2244                        }),
2245                )
2246                .into_any_element(),
2247        );
2248
2249        for entry in options.entries() {
2250            view = view.entry(entry.clone());
2251        }
2252
2253        view.render(window, cx).into_any_element()
2254    }
2255
2256    fn render_edit_wsl(
2257        &self,
2258        connection: &WslConnectionOptions,
2259        index: WslServerIndex,
2260        entries: &[NavigableEntry],
2261        window: &mut Window,
2262        cx: &mut Context<Self>,
2263    ) -> impl IntoElement {
2264        let distro_name = SharedString::new(connection.distro_name.clone());
2265
2266        v_flex().child({
2267            fn remove_wsl_distro(
2268                remote_servers: Entity<RemoteServerProjects>,
2269                index: WslServerIndex,
2270                distro_name: SharedString,
2271                window: &mut Window,
2272                cx: &mut App,
2273            ) {
2274                let prompt_message = format!("Remove WSL distro `{}`?", distro_name);
2275
2276                let confirmation = window.prompt(
2277                    PromptLevel::Warning,
2278                    &prompt_message,
2279                    None,
2280                    &["Yes, remove it", "No, keep it"],
2281                    cx,
2282                );
2283
2284                cx.spawn(async move |cx| {
2285                    if confirmation.await.ok() == Some(0) {
2286                        remote_servers.update(cx, |this, cx| {
2287                            this.delete_wsl_distro(index, cx);
2288                        });
2289                        remote_servers.update(cx, |this, cx| {
2290                            this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
2291                            cx.notify();
2292                        });
2293                    }
2294                    anyhow::Ok(())
2295                })
2296                .detach_and_log_err(cx);
2297            }
2298            div()
2299                .id("wsl-options-remove-distro")
2300                .track_focus(&entries[0].focus_handle)
2301                .on_action(cx.listener({
2302                    let distro_name = distro_name.clone();
2303                    move |_, _: &menu::Confirm, window, cx| {
2304                        remove_wsl_distro(cx.entity(), index, distro_name.clone(), window, cx);
2305                        cx.focus_self(window);
2306                    }
2307                }))
2308                .child(
2309                    ListItem::new("remove-distro")
2310                        .toggle_state(entries[0].focus_handle.contains_focused(window, cx))
2311                        .inset(true)
2312                        .spacing(ui::ListItemSpacing::Sparse)
2313                        .start_slot(Icon::new(IconName::Trash).color(Color::Error))
2314                        .child(Label::new("Remove Distro").color(Color::Error))
2315                        .on_click(cx.listener(move |_, _, window, cx| {
2316                            remove_wsl_distro(cx.entity(), index, distro_name.clone(), window, cx);
2317                            cx.focus_self(window);
2318                        })),
2319                )
2320        })
2321    }
2322
2323    fn render_edit_ssh(
2324        &self,
2325        connection: &SshConnectionOptions,
2326        index: SshServerIndex,
2327        entries: &[NavigableEntry],
2328        window: &mut Window,
2329        cx: &mut Context<Self>,
2330    ) -> impl IntoElement {
2331        let connection_string = SharedString::new(connection.host.to_string());
2332
2333        v_flex()
2334            .child({
2335                let label = if connection.nickname.is_some() {
2336                    "Edit Nickname"
2337                } else {
2338                    "Add Nickname to Server"
2339                };
2340                div()
2341                    .id("ssh-options-add-nickname")
2342                    .track_focus(&entries[0].focus_handle)
2343                    .on_action(cx.listener(move |this, _: &menu::Confirm, window, cx| {
2344                        this.mode = Mode::EditNickname(EditNicknameState::new(index, window, cx));
2345                        cx.notify();
2346                    }))
2347                    .child(
2348                        ListItem::new("add-nickname")
2349                            .toggle_state(entries[0].focus_handle.contains_focused(window, cx))
2350                            .inset(true)
2351                            .spacing(ui::ListItemSpacing::Sparse)
2352                            .start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
2353                            .child(Label::new(label))
2354                            .on_click(cx.listener(move |this, _, window, cx| {
2355                                this.mode =
2356                                    Mode::EditNickname(EditNicknameState::new(index, window, cx));
2357                                cx.notify();
2358                            })),
2359                    )
2360            })
2361            .child({
2362                let workspace = self.workspace.clone();
2363                fn callback(
2364                    workspace: WeakEntity<Workspace>,
2365                    connection_string: SharedString,
2366                    cx: &mut App,
2367                ) {
2368                    cx.write_to_clipboard(ClipboardItem::new_string(connection_string.to_string()));
2369                    workspace
2370                        .update(cx, |this, cx| {
2371                            struct SshServerAddressCopiedToClipboard;
2372                            let notification = format!(
2373                                "Copied server address ({}) to clipboard",
2374                                connection_string
2375                            );
2376
2377                            this.show_toast(
2378                                Toast::new(
2379                                    NotificationId::composite::<SshServerAddressCopiedToClipboard>(
2380                                        connection_string.clone(),
2381                                    ),
2382                                    notification,
2383                                )
2384                                .autohide(),
2385                                cx,
2386                            );
2387                        })
2388                        .ok();
2389                }
2390                div()
2391                    .id("ssh-options-copy-server-address")
2392                    .track_focus(&entries[1].focus_handle)
2393                    .on_action({
2394                        let connection_string = connection_string.clone();
2395                        let workspace = self.workspace.clone();
2396                        move |_: &menu::Confirm, _, cx| {
2397                            callback(workspace.clone(), connection_string.clone(), cx);
2398                        }
2399                    })
2400                    .child(
2401                        ListItem::new("copy-server-address")
2402                            .toggle_state(entries[1].focus_handle.contains_focused(window, cx))
2403                            .inset(true)
2404                            .spacing(ui::ListItemSpacing::Sparse)
2405                            .start_slot(Icon::new(IconName::Copy).color(Color::Muted))
2406                            .child(Label::new("Copy Server Address"))
2407                            .end_hover_slot(
2408                                Label::new(connection_string.clone()).color(Color::Muted),
2409                            )
2410                            .on_click({
2411                                let connection_string = connection_string.clone();
2412                                move |_, _, cx| {
2413                                    callback(workspace.clone(), connection_string.clone(), cx);
2414                                }
2415                            }),
2416                    )
2417            })
2418            .child({
2419                fn remove_ssh_server(
2420                    remote_servers: Entity<RemoteServerProjects>,
2421                    index: SshServerIndex,
2422                    connection_string: SharedString,
2423                    window: &mut Window,
2424                    cx: &mut App,
2425                ) {
2426                    let prompt_message = format!("Remove server `{}`?", connection_string);
2427
2428                    let confirmation = window.prompt(
2429                        PromptLevel::Warning,
2430                        &prompt_message,
2431                        None,
2432                        &["Yes, remove it", "No, keep it"],
2433                        cx,
2434                    );
2435
2436                    cx.spawn(async move |cx| {
2437                        if confirmation.await.ok() == Some(0) {
2438                            remote_servers.update(cx, |this, cx| {
2439                                this.delete_ssh_server(index, cx);
2440                            });
2441                            remote_servers.update(cx, |this, cx| {
2442                                this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
2443                                cx.notify();
2444                            });
2445                        }
2446                        anyhow::Ok(())
2447                    })
2448                    .detach_and_log_err(cx);
2449                }
2450                div()
2451                    .id("ssh-options-copy-server-address")
2452                    .track_focus(&entries[2].focus_handle)
2453                    .on_action(cx.listener({
2454                        let connection_string = connection_string.clone();
2455                        move |_, _: &menu::Confirm, window, cx| {
2456                            remove_ssh_server(
2457                                cx.entity(),
2458                                index,
2459                                connection_string.clone(),
2460                                window,
2461                                cx,
2462                            );
2463                            cx.focus_self(window);
2464                        }
2465                    }))
2466                    .child(
2467                        ListItem::new("remove-server")
2468                            .toggle_state(entries[2].focus_handle.contains_focused(window, cx))
2469                            .inset(true)
2470                            .spacing(ui::ListItemSpacing::Sparse)
2471                            .start_slot(Icon::new(IconName::Trash).color(Color::Error))
2472                            .child(Label::new("Remove Server").color(Color::Error))
2473                            .on_click(cx.listener(move |_, _, window, cx| {
2474                                remove_ssh_server(
2475                                    cx.entity(),
2476                                    index,
2477                                    connection_string.clone(),
2478                                    window,
2479                                    cx,
2480                                );
2481                                cx.focus_self(window);
2482                            })),
2483                    )
2484            })
2485    }
2486
2487    fn render_edit_nickname(
2488        &self,
2489        state: &EditNicknameState,
2490        window: &mut Window,
2491        cx: &mut Context<Self>,
2492    ) -> impl IntoElement {
2493        let Some(connection) = RemoteSettings::get_global(cx)
2494            .ssh_connections()
2495            .nth(state.index.0)
2496        else {
2497            return v_flex()
2498                .id("ssh-edit-nickname")
2499                .track_focus(&self.focus_handle(cx));
2500        };
2501
2502        let connection_string = connection.host.clone();
2503        let nickname = connection.nickname.map(|s| s.into());
2504
2505        v_flex()
2506            .id("ssh-edit-nickname")
2507            .track_focus(&self.focus_handle(cx))
2508            .child(
2509                SshConnectionHeader {
2510                    connection_string: connection_string.into(),
2511                    paths: Default::default(),
2512                    nickname,
2513                    is_wsl: false,
2514                    is_devcontainer: false,
2515                }
2516                .render(window, cx),
2517            )
2518            .child(
2519                h_flex()
2520                    .p_2()
2521                    .border_t_1()
2522                    .border_color(cx.theme().colors().border_variant)
2523                    .child(state.editor.clone()),
2524            )
2525    }
2526
2527    fn render_default(
2528        &mut self,
2529        mut state: DefaultState,
2530        window: &mut Window,
2531        cx: &mut Context<Self>,
2532    ) -> impl IntoElement {
2533        let ssh_settings = RemoteSettings::get_global(cx);
2534        let mut should_rebuild = false;
2535
2536        let ssh_connections_changed = ssh_settings.ssh_connections.0.iter().ne(state
2537            .servers
2538            .iter()
2539            .filter_map(|server| match server {
2540                RemoteEntry::Project {
2541                    connection: Connection::Ssh(connection),
2542                    ..
2543                } => Some(connection),
2544                _ => None,
2545            }));
2546
2547        let wsl_connections_changed = ssh_settings.wsl_connections.0.iter().ne(state
2548            .servers
2549            .iter()
2550            .filter_map(|server| match server {
2551                RemoteEntry::Project {
2552                    connection: Connection::Wsl(connection),
2553                    ..
2554                } => Some(connection),
2555                _ => None,
2556            }));
2557
2558        if ssh_connections_changed || wsl_connections_changed {
2559            should_rebuild = true;
2560        };
2561
2562        if !should_rebuild && ssh_settings.read_ssh_config {
2563            let current_ssh_hosts: BTreeSet<SharedString> = state
2564                .servers
2565                .iter()
2566                .filter_map(|server| match server {
2567                    RemoteEntry::SshConfig { host, .. } => Some(host.clone()),
2568                    _ => None,
2569                })
2570                .collect();
2571            let mut expected_ssh_hosts = self.ssh_config_servers.clone();
2572            for server in &state.servers {
2573                if let RemoteEntry::Project {
2574                    connection: Connection::Ssh(connection),
2575                    ..
2576                } = server
2577                {
2578                    expected_ssh_hosts.remove(connection.host.as_str());
2579                }
2580            }
2581            should_rebuild = current_ssh_hosts != expected_ssh_hosts;
2582        }
2583
2584        if should_rebuild {
2585            self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
2586            if let Mode::Default(new_state) = &self.mode {
2587                state = new_state.clone();
2588            }
2589        }
2590
2591        let connect_button = div()
2592            .id("ssh-connect-new-server-container")
2593            .track_focus(&state.add_new_server.focus_handle)
2594            .anchor_scroll(state.add_new_server.scroll_anchor.clone())
2595            .child(
2596                ListItem::new("register-remote-server-button")
2597                    .toggle_state(
2598                        state
2599                            .add_new_server
2600                            .focus_handle
2601                            .contains_focused(window, cx),
2602                    )
2603                    .inset(true)
2604                    .spacing(ui::ListItemSpacing::Sparse)
2605                    .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
2606                    .child(Label::new("Connect SSH Server"))
2607                    .on_click(cx.listener(|this, _, window, cx| {
2608                        let state = CreateRemoteServer::new(window, cx);
2609                        this.mode = Mode::CreateRemoteServer(state);
2610
2611                        cx.notify();
2612                    })),
2613            )
2614            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
2615                let state = CreateRemoteServer::new(window, cx);
2616                this.mode = Mode::CreateRemoteServer(state);
2617
2618                cx.notify();
2619            }));
2620
2621        let connect_dev_container_button = div()
2622            .id("connect-new-dev-container")
2623            .track_focus(&state.add_new_devcontainer.focus_handle)
2624            .anchor_scroll(state.add_new_devcontainer.scroll_anchor.clone())
2625            .child(
2626                ListItem::new("register-dev-container-button")
2627                    .toggle_state(
2628                        state
2629                            .add_new_devcontainer
2630                            .focus_handle
2631                            .contains_focused(window, cx),
2632                    )
2633                    .inset(true)
2634                    .spacing(ui::ListItemSpacing::Sparse)
2635                    .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
2636                    .child(Label::new("Connect Dev Container"))
2637                    .on_click(cx.listener(|this, _, window, cx| {
2638                        this.init_dev_container_mode(window, cx);
2639                    })),
2640            )
2641            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
2642                this.init_dev_container_mode(window, cx);
2643            }));
2644
2645        #[cfg(target_os = "windows")]
2646        let wsl_connect_button = div()
2647            .id("wsl-connect-new-server")
2648            .track_focus(&state.add_new_wsl.focus_handle)
2649            .anchor_scroll(state.add_new_wsl.scroll_anchor.clone())
2650            .child(
2651                ListItem::new("wsl-add-new-server")
2652                    .toggle_state(state.add_new_wsl.focus_handle.contains_focused(window, cx))
2653                    .inset(true)
2654                    .spacing(ui::ListItemSpacing::Sparse)
2655                    .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
2656                    .child(Label::new("Add WSL Distro"))
2657                    .on_click(cx.listener(|this, _, window, cx| {
2658                        let state = AddWslDistro::new(window, cx);
2659                        this.mode = Mode::AddWslDistro(state);
2660
2661                        cx.notify();
2662                    })),
2663            )
2664            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
2665                let state = AddWslDistro::new(window, cx);
2666                this.mode = Mode::AddWslDistro(state);
2667
2668                cx.notify();
2669            }));
2670
2671        let has_open_project = self
2672            .workspace
2673            .upgrade()
2674            .map(|workspace| {
2675                workspace
2676                    .read(cx)
2677                    .project()
2678                    .read(cx)
2679                    .visible_worktrees(cx)
2680                    .next()
2681                    .is_some()
2682            })
2683            .unwrap_or(false);
2684
2685        // We cannot currently connect a dev container from within a remote server due to the remote_server architecture
2686        let is_local = self
2687            .workspace
2688            .upgrade()
2689            .map(|workspace| workspace.read(cx).project().read(cx).is_local())
2690            .unwrap_or(true);
2691
2692        let modal_section = v_flex()
2693            .track_focus(&self.focus_handle(cx))
2694            .id("ssh-server-list")
2695            .overflow_y_scroll()
2696            .track_scroll(&state.scroll_handle)
2697            .size_full()
2698            .child(connect_button)
2699            .when(has_open_project && is_local, |this| {
2700                this.child(connect_dev_container_button)
2701            });
2702
2703        #[cfg(target_os = "windows")]
2704        let modal_section = modal_section.child(wsl_connect_button);
2705        #[cfg(not(target_os = "windows"))]
2706        let modal_section = modal_section;
2707
2708        let mut modal_section = Navigable::new(
2709            modal_section
2710                .child(
2711                    List::new()
2712                        .empty_message(
2713                            h_flex()
2714                                .size_full()
2715                                .p_2()
2716                                .justify_center()
2717                                .border_t_1()
2718                                .border_color(cx.theme().colors().border_variant)
2719                                .child(
2720                                    Label::new("No remote servers registered yet.")
2721                                        .color(Color::Muted),
2722                                )
2723                                .into_any_element(),
2724                        )
2725                        .children(state.servers.iter().enumerate().map(|(ix, connection)| {
2726                            self.render_remote_connection(ix, connection.clone(), window, cx)
2727                                .into_any_element()
2728                        })),
2729                )
2730                .into_any_element(),
2731        )
2732        .entry(state.add_new_server.clone());
2733
2734        if has_open_project && is_local {
2735            modal_section = modal_section.entry(state.add_new_devcontainer.clone());
2736        }
2737
2738        if cfg!(target_os = "windows") {
2739            modal_section = modal_section.entry(state.add_new_wsl.clone());
2740        }
2741
2742        for server in &state.servers {
2743            match server {
2744                RemoteEntry::Project {
2745                    open_folder,
2746                    projects,
2747                    configure,
2748                    ..
2749                } => {
2750                    for (navigation_state, _) in projects {
2751                        modal_section = modal_section.entry(navigation_state.clone());
2752                    }
2753                    modal_section = modal_section
2754                        .entry(open_folder.clone())
2755                        .entry(configure.clone());
2756                }
2757                RemoteEntry::SshConfig { open_folder, .. } => {
2758                    modal_section = modal_section.entry(open_folder.clone());
2759                }
2760            }
2761        }
2762        let mut modal_section = modal_section.render(window, cx).into_any_element();
2763
2764        let is_project_selected = state.servers.iter().any(|server| match server {
2765            RemoteEntry::Project { projects, .. } => projects
2766                .iter()
2767                .any(|(entry, _)| entry.focus_handle.contains_focused(window, cx)),
2768            RemoteEntry::SshConfig { .. } => false,
2769        });
2770
2771        Modal::new("remote-projects", None)
2772            .header(ModalHeader::new().headline("Remote Projects"))
2773            .section(
2774                Section::new().padded(false).child(
2775                    v_flex()
2776                        .min_h(rems(20.))
2777                        .size_full()
2778                        .relative()
2779                        .child(ListSeparator)
2780                        .child(
2781                            canvas(
2782                                |bounds, window, cx| {
2783                                    modal_section.prepaint_as_root(
2784                                        bounds.origin,
2785                                        bounds.size.into(),
2786                                        window,
2787                                        cx,
2788                                    );
2789                                    modal_section
2790                                },
2791                                |_, mut modal_section, window, cx| {
2792                                    modal_section.paint(window, cx);
2793                                },
2794                            )
2795                            .size_full(),
2796                        )
2797                        .vertical_scrollbar_for(&state.scroll_handle, window, cx),
2798                ),
2799            )
2800            .footer(ModalFooter::new().end_slot({
2801                let confirm_button = |label: SharedString| {
2802                    Button::new("select", label)
2803                        .key_binding(KeyBinding::for_action(&menu::Confirm, cx))
2804                        .on_click(|_, window, cx| {
2805                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)
2806                        })
2807                };
2808
2809                if is_project_selected {
2810                    h_flex()
2811                        .gap_1()
2812                        .child(
2813                            Button::new("open_new_window", "New Window")
2814                                .key_binding(KeyBinding::for_action(&menu::SecondaryConfirm, cx))
2815                                .on_click(|_, window, cx| {
2816                                    window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
2817                                }),
2818                        )
2819                        .child(confirm_button("Open".into()))
2820                        .into_any_element()
2821                } else {
2822                    confirm_button("Select".into()).into_any_element()
2823                }
2824            }))
2825            .into_any_element()
2826    }
2827
2828    fn create_host_from_ssh_config(
2829        &mut self,
2830        ssh_config_host: &SharedString,
2831        cx: &mut Context<'_, Self>,
2832    ) -> SshServerIndex {
2833        let new_ix = Arc::new(AtomicUsize::new(0));
2834
2835        let update_new_ix = new_ix.clone();
2836        self.update_settings_file(cx, move |settings, _| {
2837            update_new_ix.store(
2838                settings
2839                    .ssh_connections
2840                    .as_ref()
2841                    .map_or(0, |connections| connections.len()),
2842                atomic::Ordering::Release,
2843            );
2844        });
2845
2846        self.add_ssh_server(
2847            SshConnectionOptions {
2848                host: ssh_config_host.to_string().into(),
2849                ..SshConnectionOptions::default()
2850            },
2851            cx,
2852        );
2853        self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
2854        SshServerIndex(new_ix.load(atomic::Ordering::Acquire))
2855    }
2856}
2857
2858fn spawn_ssh_config_watch(fs: Arc<dyn Fs>, cx: &Context<RemoteServerProjects>) -> Task<()> {
2859    enum ConfigSource {
2860        User(String),
2861        Global(String),
2862    }
2863
2864    let mut streams = Vec::new();
2865    let mut tasks = Vec::new();
2866
2867    // Setup User Watcher
2868    let user_path = user_ssh_config_file();
2869    info!("SSH: Watching User Config at: {:?}", user_path);
2870
2871    // We clone 'fs' here because we might need it again for the global watcher.
2872    let (user_s, user_t) = watch_config_file(cx.background_executor(), fs.clone(), user_path);
2873    streams.push(user_s.map(ConfigSource::User).boxed());
2874    tasks.push(user_t);
2875
2876    // Setup Global Watcher
2877    if let Some(gp) = global_ssh_config_file() {
2878        info!("SSH: Watching Global Config at: {:?}", gp);
2879        let (global_s, global_t) =
2880            watch_config_file(cx.background_executor(), fs, gp.to_path_buf());
2881        streams.push(global_s.map(ConfigSource::Global).boxed());
2882        tasks.push(global_t);
2883    } else {
2884        debug!("SSH: No Global Config defined.");
2885    }
2886
2887    // Combine into a single stream so that only one is parsed at once.
2888    let mut merged_stream = futures::stream::select_all(streams);
2889
2890    cx.spawn(async move |remote_server_projects, cx| {
2891        let _tasks = tasks; // Keeps the background watchers alive
2892        let mut global_hosts = BTreeSet::default();
2893        let mut user_hosts = BTreeSet::default();
2894
2895        while let Some(event) = merged_stream.next().await {
2896            match event {
2897                ConfigSource::Global(content) => {
2898                    global_hosts = parse_ssh_config_hosts(&content);
2899                }
2900                ConfigSource::User(content) => {
2901                    user_hosts = parse_ssh_config_hosts(&content);
2902                }
2903            }
2904
2905            // Sync to Model
2906            if remote_server_projects
2907                .update(cx, |project, cx| {
2908                    project.ssh_config_servers = global_hosts
2909                        .iter()
2910                        .chain(user_hosts.iter())
2911                        .map(SharedString::from)
2912                        .collect();
2913                    cx.notify();
2914                })
2915                .is_err()
2916            {
2917                return;
2918            }
2919        }
2920    })
2921}
2922
2923fn get_text(element: &Entity<Editor>, cx: &mut App) -> String {
2924    element.read(cx).text(cx).trim().to_string()
2925}
2926
2927impl ModalView for RemoteServerProjects {}
2928
2929impl Focusable for RemoteServerProjects {
2930    fn focus_handle(&self, cx: &App) -> FocusHandle {
2931        match &self.mode {
2932            Mode::ProjectPicker(picker) => picker.focus_handle(cx),
2933            _ => self.focus_handle.clone(),
2934        }
2935    }
2936}
2937
2938impl EventEmitter<DismissEvent> for RemoteServerProjects {}
2939
2940impl Render for RemoteServerProjects {
2941    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2942        div()
2943            .elevation_3(cx)
2944            .w(rems(34.))
2945            .key_context("RemoteServerModal")
2946            .on_action(cx.listener(Self::cancel))
2947            .on_action(cx.listener(Self::confirm))
2948            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
2949                this.focus_handle(cx).focus(window, cx);
2950            }))
2951            .on_mouse_down_out(cx.listener(|this, _, _, cx| {
2952                if matches!(this.mode, Mode::Default(_)) {
2953                    cx.emit(DismissEvent)
2954                }
2955            }))
2956            .child(match &self.mode {
2957                Mode::Default(state) => self
2958                    .render_default(state.clone(), window, cx)
2959                    .into_any_element(),
2960                Mode::ViewServerOptions(state) => self
2961                    .render_view_options(state.clone(), window, cx)
2962                    .into_any_element(),
2963                Mode::ProjectPicker(element) => element.clone().into_any_element(),
2964                Mode::CreateRemoteServer(state) => self
2965                    .render_create_remote_server(state, window, cx)
2966                    .into_any_element(),
2967                Mode::CreateRemoteDevContainer(state) => self
2968                    .render_create_dev_container(state, window, cx)
2969                    .into_any_element(),
2970                Mode::EditNickname(state) => self
2971                    .render_edit_nickname(state, window, cx)
2972                    .into_any_element(),
2973                #[cfg(target_os = "windows")]
2974                Mode::AddWslDistro(state) => self
2975                    .render_add_wsl_distro(state, window, cx)
2976                    .into_any_element(),
2977            })
2978    }
2979}