remote_servers.rs

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