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.get(server.0).is_some()
1661            {
1662                connections.remove(server.0);
1663            }
1664        });
1665    }
1666
1667    fn delete_remote_project(
1668        &mut self,
1669        server: ServerIndex,
1670        project: &RemoteProject,
1671        cx: &mut Context<Self>,
1672    ) {
1673        match server {
1674            ServerIndex::Ssh(server) => {
1675                self.delete_ssh_project(server, project, cx);
1676            }
1677            ServerIndex::Wsl(server) => {
1678                self.delete_wsl_project(server, project, cx);
1679            }
1680        }
1681    }
1682
1683    fn delete_ssh_project(
1684        &mut self,
1685        server: SshServerIndex,
1686        project: &RemoteProject,
1687        cx: &mut Context<Self>,
1688    ) {
1689        let project = project.clone();
1690        self.update_settings_file(cx, move |setting, _| {
1691            if let Some(server) = setting
1692                .ssh_connections
1693                .as_mut()
1694                .and_then(|connections| connections.get_mut(server.0))
1695            {
1696                server.projects.remove(&project);
1697            }
1698        });
1699    }
1700
1701    fn delete_wsl_project(
1702        &mut self,
1703        server: WslServerIndex,
1704        project: &RemoteProject,
1705        cx: &mut Context<Self>,
1706    ) {
1707        let project = project.clone();
1708        self.update_settings_file(cx, move |setting, _| {
1709            if let Some(server) = setting
1710                .wsl_connections
1711                .as_mut()
1712                .and_then(|connections| connections.get_mut(server.0))
1713            {
1714                server.projects.remove(&project);
1715            }
1716        });
1717    }
1718
1719    fn delete_wsl_distro(&mut self, server: WslServerIndex, cx: &mut Context<Self>) {
1720        self.update_settings_file(cx, move |setting, _| {
1721            if let Some(connections) = setting.wsl_connections.as_mut() {
1722                connections.remove(server.0);
1723            }
1724        });
1725    }
1726
1727    fn add_ssh_server(
1728        &mut self,
1729        connection_options: remote::SshConnectionOptions,
1730        cx: &mut Context<Self>,
1731    ) {
1732        self.update_settings_file(cx, move |setting, _| {
1733            setting
1734                .ssh_connections
1735                .get_or_insert(Default::default())
1736                .push(SshConnection {
1737                    host: connection_options.host.to_string(),
1738                    username: connection_options.username,
1739                    port: connection_options.port,
1740                    projects: BTreeSet::new(),
1741                    nickname: None,
1742                    args: connection_options.args.unwrap_or_default(),
1743                    upload_binary_over_ssh: None,
1744                    port_forwards: connection_options.port_forwards,
1745                    connection_timeout: connection_options.connection_timeout,
1746                })
1747        });
1748    }
1749
1750    fn edit_in_dev_container_json(
1751        &mut self,
1752        config: Option<DevContainerConfig>,
1753        window: &mut Window,
1754        cx: &mut Context<Self>,
1755    ) {
1756        let Some(workspace) = self.workspace.upgrade() else {
1757            cx.emit(DismissEvent);
1758            cx.notify();
1759            return;
1760        };
1761
1762        let config_path = config
1763            .map(|c| c.config_path)
1764            .unwrap_or_else(|| PathBuf::from(".devcontainer/devcontainer.json"));
1765
1766        workspace.update(cx, |workspace, cx| {
1767            let project = workspace.project().clone();
1768
1769            let worktree = project
1770                .read(cx)
1771                .visible_worktrees(cx)
1772                .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
1773
1774            if let Some(worktree) = worktree {
1775                let tree_id = worktree.read(cx).id();
1776                let devcontainer_path =
1777                    match RelPath::new(&config_path, util::paths::PathStyle::Posix) {
1778                        Ok(path) => path.into_owned(),
1779                        Err(error) => {
1780                            log::error!(
1781                                "Invalid devcontainer path: {} - {}",
1782                                config_path.display(),
1783                                error
1784                            );
1785                            return;
1786                        }
1787                    };
1788                cx.spawn_in(window, async move |workspace, cx| {
1789                    workspace
1790                        .update_in(cx, |workspace, window, cx| {
1791                            workspace.open_path(
1792                                (tree_id, devcontainer_path),
1793                                None,
1794                                true,
1795                                window,
1796                                cx,
1797                            )
1798                        })?
1799                        .await
1800                })
1801                .detach();
1802            } else {
1803                return;
1804            }
1805        });
1806        cx.emit(DismissEvent);
1807        cx.notify();
1808    }
1809
1810    fn init_dev_container_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1811        let configs = self
1812            .workspace
1813            .read_with(cx, |workspace, cx| find_devcontainer_configs(workspace, cx))
1814            .unwrap_or_default();
1815
1816        if configs.len() > 1 {
1817            let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
1818            self.dev_container_picker =
1819                Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)));
1820
1821            let state =
1822                CreateRemoteDevContainer::new(DevContainerCreationProgress::SelectingConfig, cx);
1823            self.mode = Mode::CreateRemoteDevContainer(state);
1824            cx.notify();
1825        } else if let Some((app_state, context)) = self
1826            .workspace
1827            .read_with(cx, |workspace, cx| {
1828                let app_state = workspace.app_state().clone();
1829                let context = DevContainerContext::from_workspace(workspace, cx)?;
1830                Some((app_state, context))
1831            })
1832            .ok()
1833            .flatten()
1834        {
1835            let config = configs.into_iter().next();
1836            self.open_dev_container(config, app_state, context, window, cx);
1837            self.view_in_progress_dev_container(window, cx);
1838        } else {
1839            log::error!("No active project directory for Dev Container");
1840        }
1841    }
1842
1843    fn open_dev_container(
1844        &self,
1845        config: Option<DevContainerConfig>,
1846        app_state: Arc<AppState>,
1847        context: DevContainerContext,
1848        window: &mut Window,
1849        cx: &mut Context<Self>,
1850    ) {
1851        let replace_window = window.window_handle().downcast::<MultiWorkspace>();
1852
1853        let app_state = Arc::downgrade(&app_state);
1854        cx.spawn_in(window, async move |entity, cx| {
1855            let (connection, starting_dir) =
1856                match start_dev_container_with_config(context, config).await {
1857                    Ok((c, s)) => (Connection::DevContainer(c), s),
1858                    Err(e) => {
1859                        log::error!("Failed to start dev container: {:?}", e);
1860                        cx.prompt(
1861                            gpui::PromptLevel::Critical,
1862                            "Failed to start Dev Container. See logs for details",
1863                            Some(&format!("{e}")),
1864                            &["Ok"],
1865                        )
1866                        .await
1867                        .ok();
1868                        entity
1869                            .update_in(cx, |remote_server_projects, window, cx| {
1870                                remote_server_projects.mode =
1871                                    Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(
1872                                        DevContainerCreationProgress::Error(format!("{e}")),
1873                                        cx,
1874                                    ));
1875                                remote_server_projects.focus_handle(cx).focus(window, cx);
1876                            })
1877                            .ok();
1878                        return;
1879                    }
1880                };
1881            entity
1882                .update(cx, |_, cx| {
1883                    cx.emit(DismissEvent);
1884                })
1885                .log_err();
1886
1887            let Some(app_state) = app_state.upgrade() else {
1888                return;
1889            };
1890            let result = open_remote_project(
1891                connection.into(),
1892                vec![starting_dir].into_iter().map(PathBuf::from).collect(),
1893                app_state,
1894                OpenOptions {
1895                    replace_window,
1896                    ..OpenOptions::default()
1897                },
1898                cx,
1899            )
1900            .await;
1901            if let Err(e) = result {
1902                log::error!("Failed to connect: {e:#}");
1903                cx.prompt(
1904                    gpui::PromptLevel::Critical,
1905                    "Failed to connect",
1906                    Some(&e.to_string()),
1907                    &["Ok"],
1908                )
1909                .await
1910                .ok();
1911            }
1912        })
1913        .detach();
1914    }
1915
1916    fn render_create_dev_container(
1917        &self,
1918        state: &CreateRemoteDevContainer,
1919        window: &mut Window,
1920        cx: &mut Context<Self>,
1921    ) -> impl IntoElement {
1922        match &state.progress {
1923            DevContainerCreationProgress::Error(message) => {
1924                let view = Navigable::new(
1925                    div()
1926                        .child(
1927                            div().track_focus(&self.focus_handle(cx)).size_full().child(
1928                                v_flex().py_1().child(
1929                                    ListItem::new("Error")
1930                                        .inset(true)
1931                                        .selectable(false)
1932                                        .spacing(ui::ListItemSpacing::Sparse)
1933                                        .start_slot(
1934                                            Icon::new(IconName::XCircle).color(Color::Error),
1935                                        )
1936                                        .child(Label::new("Error Creating Dev Container:"))
1937                                        .child(Label::new(message).buffer_font(cx)),
1938                                ),
1939                            ),
1940                        )
1941                        .child(ListSeparator)
1942                        .child(
1943                            div()
1944                                .id("devcontainer-see-log")
1945                                .track_focus(&state.view_logs_entry.focus_handle)
1946                                .on_action(cx.listener(|_, _: &menu::Confirm, window, cx| {
1947                                    window.dispatch_action(Box::new(OpenLog), cx);
1948                                    cx.emit(DismissEvent);
1949                                    cx.notify();
1950                                }))
1951                                .child(
1952                                    ListItem::new("li-devcontainer-see-log")
1953                                        .toggle_state(
1954                                            state
1955                                                .view_logs_entry
1956                                                .focus_handle
1957                                                .contains_focused(window, cx),
1958                                        )
1959                                        .inset(true)
1960                                        .spacing(ui::ListItemSpacing::Sparse)
1961                                        .start_slot(Icon::new(IconName::File).color(Color::Muted))
1962                                        .child(Label::new("Open Zed Log"))
1963                                        .on_click(cx.listener(|_, _, window, cx| {
1964                                            window.dispatch_action(Box::new(OpenLog), cx);
1965                                            cx.emit(DismissEvent);
1966                                            cx.notify();
1967                                        })),
1968                                ),
1969                        )
1970                        .child(
1971                            div()
1972                                .id("devcontainer-go-back")
1973                                .track_focus(&state.back_entry.focus_handle)
1974                                .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
1975                                    this.cancel(&menu::Cancel, window, cx);
1976                                    cx.notify();
1977                                }))
1978                                .child(
1979                                    ListItem::new("li-devcontainer-go-back")
1980                                        .toggle_state(
1981                                            state
1982                                                .back_entry
1983                                                .focus_handle
1984                                                .contains_focused(window, cx),
1985                                        )
1986                                        .inset(true)
1987                                        .spacing(ui::ListItemSpacing::Sparse)
1988                                        .start_slot(Icon::new(IconName::Exit).color(Color::Muted))
1989                                        .child(Label::new("Exit"))
1990                                        .on_click(cx.listener(|this, _, window, cx| {
1991                                            this.cancel(&menu::Cancel, window, cx);
1992                                            cx.notify();
1993                                        })),
1994                                ),
1995                        )
1996                        .into_any_element(),
1997                )
1998                .entry(state.view_logs_entry.clone())
1999                .entry(state.back_entry.clone());
2000                view.render(window, cx).into_any_element()
2001            }
2002            DevContainerCreationProgress::SelectingConfig => {
2003                self.render_config_selection(window, cx).into_any_element()
2004            }
2005            DevContainerCreationProgress::Creating => {
2006                self.focus_handle(cx).focus(window, cx);
2007                div()
2008                    .track_focus(&self.focus_handle(cx))
2009                    .size_full()
2010                    .child(
2011                        v_flex()
2012                            .pb_1()
2013                            .child(
2014                                ModalHeader::new().child(
2015                                    Headline::new("Dev Containers").size(HeadlineSize::XSmall),
2016                                ),
2017                            )
2018                            .child(ListSeparator)
2019                            .child(
2020                                ListItem::new("creating")
2021                                    .inset(true)
2022                                    .spacing(ui::ListItemSpacing::Sparse)
2023                                    .disabled(true)
2024                                    .start_slot(
2025                                        Icon::new(IconName::ArrowCircle)
2026                                            .color(Color::Muted)
2027                                            .with_rotate_animation(2),
2028                                    )
2029                                    .child(
2030                                        h_flex()
2031                                            .opacity(0.6)
2032                                            .gap_1()
2033                                            .child(Label::new("Creating Dev Container"))
2034                                            .child(LoadingLabel::new("")),
2035                                    ),
2036                            ),
2037                    )
2038                    .into_any_element()
2039            }
2040        }
2041    }
2042
2043    fn render_config_selection(
2044        &self,
2045        window: &mut Window,
2046        cx: &mut Context<Self>,
2047    ) -> impl IntoElement {
2048        let Some(picker) = &self.dev_container_picker else {
2049            return div().into_any_element();
2050        };
2051
2052        let content = v_flex().pb_1().child(picker.clone().into_any_element());
2053
2054        picker.focus_handle(cx).focus(window, cx);
2055
2056        content.into_any_element()
2057    }
2058
2059    fn render_create_remote_server(
2060        &self,
2061        state: &CreateRemoteServer,
2062        window: &mut Window,
2063        cx: &mut Context<Self>,
2064    ) -> impl IntoElement {
2065        let ssh_prompt = state.ssh_prompt.clone();
2066
2067        state.address_editor.update(cx, |editor, cx| {
2068            if editor.text(cx).is_empty() {
2069                editor.set_placeholder_text("ssh user@example -p 2222", window, cx);
2070            }
2071        });
2072
2073        let theme = cx.theme();
2074
2075        v_flex()
2076            .track_focus(&self.focus_handle(cx))
2077            .id("create-remote-server")
2078            .overflow_hidden()
2079            .size_full()
2080            .flex_1()
2081            .child(
2082                div()
2083                    .p_2()
2084                    .border_b_1()
2085                    .border_color(theme.colors().border_variant)
2086                    .child(state.address_editor.clone()),
2087            )
2088            .child(
2089                h_flex()
2090                    .bg(theme.colors().editor_background)
2091                    .rounded_b_sm()
2092                    .w_full()
2093                    .map(|this| {
2094                        if let Some(ssh_prompt) = ssh_prompt {
2095                            this.child(h_flex().w_full().child(ssh_prompt))
2096                        } else if let Some(address_error) = &state.address_error {
2097                            this.child(
2098                                h_flex().p_2().w_full().gap_2().child(
2099                                    Label::new(address_error.clone())
2100                                        .size(LabelSize::Small)
2101                                        .color(Color::Error),
2102                                ),
2103                            )
2104                        } else {
2105                            this.child(
2106                                h_flex()
2107                                    .p_2()
2108                                    .w_full()
2109                                    .gap_1()
2110                                    .child(
2111                                        Label::new(
2112                                            "Enter the command you use to SSH into this server.",
2113                                        )
2114                                        .color(Color::Muted)
2115                                        .size(LabelSize::Small),
2116                                    )
2117                                    .child(
2118                                        Button::new("learn-more", "Learn More")
2119                                            .label_size(LabelSize::Small)
2120                                            .icon(IconName::ArrowUpRight)
2121                                            .icon_size(IconSize::XSmall)
2122                                            .on_click(|_, _, cx| {
2123                                                cx.open_url(
2124                                                    "https://zed.dev/docs/remote-development",
2125                                                );
2126                                            }),
2127                                    ),
2128                            )
2129                        }
2130                    }),
2131            )
2132    }
2133
2134    #[cfg(target_os = "windows")]
2135    fn render_add_wsl_distro(
2136        &self,
2137        state: &AddWslDistro,
2138        window: &mut Window,
2139        cx: &mut Context<Self>,
2140    ) -> impl IntoElement {
2141        let connection_prompt = state.connection_prompt.clone();
2142
2143        state.picker.update(cx, |picker, cx| {
2144            picker.focus_handle(cx).focus(window, cx);
2145        });
2146
2147        v_flex()
2148            .id("add-wsl-distro")
2149            .overflow_hidden()
2150            .size_full()
2151            .flex_1()
2152            .map(|this| {
2153                if let Some(connection_prompt) = connection_prompt {
2154                    this.child(connection_prompt)
2155                } else {
2156                    this.child(state.picker.clone())
2157                }
2158            })
2159    }
2160
2161    fn render_view_options(
2162        &mut self,
2163        options: ViewServerOptionsState,
2164        window: &mut Window,
2165        cx: &mut Context<Self>,
2166    ) -> impl IntoElement {
2167        let last_entry = options.entries().last().unwrap();
2168
2169        let mut view = Navigable::new(
2170            div()
2171                .track_focus(&self.focus_handle(cx))
2172                .size_full()
2173                .child(match &options {
2174                    ViewServerOptionsState::Ssh { connection, .. } => SshConnectionHeader {
2175                        connection_string: connection.host.to_string().into(),
2176                        paths: Default::default(),
2177                        nickname: connection.nickname.clone().map(|s| s.into()),
2178                        is_wsl: false,
2179                        is_devcontainer: false,
2180                    }
2181                    .render(window, cx)
2182                    .into_any_element(),
2183                    ViewServerOptionsState::Wsl { connection, .. } => SshConnectionHeader {
2184                        connection_string: connection.distro_name.clone().into(),
2185                        paths: Default::default(),
2186                        nickname: None,
2187                        is_wsl: true,
2188                        is_devcontainer: false,
2189                    }
2190                    .render(window, cx)
2191                    .into_any_element(),
2192                })
2193                .child(
2194                    v_flex()
2195                        .pb_1()
2196                        .child(ListSeparator)
2197                        .map(|this| match &options {
2198                            ViewServerOptionsState::Ssh {
2199                                connection,
2200                                entries,
2201                                server_index,
2202                            } => this.child(self.render_edit_ssh(
2203                                connection,
2204                                *server_index,
2205                                entries,
2206                                window,
2207                                cx,
2208                            )),
2209                            ViewServerOptionsState::Wsl {
2210                                connection,
2211                                entries,
2212                                server_index,
2213                            } => this.child(self.render_edit_wsl(
2214                                connection,
2215                                *server_index,
2216                                entries,
2217                                window,
2218                                cx,
2219                            )),
2220                        })
2221                        .child(ListSeparator)
2222                        .child({
2223                            div()
2224                                .id("ssh-options-copy-server-address")
2225                                .track_focus(&last_entry.focus_handle)
2226                                .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
2227                                    this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
2228                                    cx.focus_self(window);
2229                                    cx.notify();
2230                                }))
2231                                .child(
2232                                    ListItem::new("go-back")
2233                                        .toggle_state(
2234                                            last_entry.focus_handle.contains_focused(window, cx),
2235                                        )
2236                                        .inset(true)
2237                                        .spacing(ui::ListItemSpacing::Sparse)
2238                                        .start_slot(
2239                                            Icon::new(IconName::ArrowLeft).color(Color::Muted),
2240                                        )
2241                                        .child(Label::new("Go Back"))
2242                                        .on_click(cx.listener(|this, _, window, cx| {
2243                                            this.mode =
2244                                                Mode::default_mode(&this.ssh_config_servers, cx);
2245                                            cx.focus_self(window);
2246                                            cx.notify()
2247                                        })),
2248                                )
2249                        }),
2250                )
2251                .into_any_element(),
2252        );
2253
2254        for entry in options.entries() {
2255            view = view.entry(entry.clone());
2256        }
2257
2258        view.render(window, cx).into_any_element()
2259    }
2260
2261    fn render_edit_wsl(
2262        &self,
2263        connection: &WslConnectionOptions,
2264        index: WslServerIndex,
2265        entries: &[NavigableEntry],
2266        window: &mut Window,
2267        cx: &mut Context<Self>,
2268    ) -> impl IntoElement {
2269        let distro_name = SharedString::new(connection.distro_name.clone());
2270
2271        v_flex().child({
2272            fn remove_wsl_distro(
2273                remote_servers: Entity<RemoteServerProjects>,
2274                index: WslServerIndex,
2275                distro_name: SharedString,
2276                window: &mut Window,
2277                cx: &mut App,
2278            ) {
2279                let prompt_message = format!("Remove WSL distro `{}`?", distro_name);
2280
2281                let confirmation = window.prompt(
2282                    PromptLevel::Warning,
2283                    &prompt_message,
2284                    None,
2285                    &["Yes, remove it", "No, keep it"],
2286                    cx,
2287                );
2288
2289                cx.spawn(async move |cx| {
2290                    if confirmation.await.ok() == Some(0) {
2291                        remote_servers.update(cx, |this, cx| {
2292                            this.delete_wsl_distro(index, cx);
2293                        });
2294                        remote_servers.update(cx, |this, cx| {
2295                            this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
2296                            cx.notify();
2297                        });
2298                    }
2299                    anyhow::Ok(())
2300                })
2301                .detach_and_log_err(cx);
2302            }
2303            div()
2304                .id("wsl-options-remove-distro")
2305                .track_focus(&entries[0].focus_handle)
2306                .on_action(cx.listener({
2307                    let distro_name = distro_name.clone();
2308                    move |_, _: &menu::Confirm, window, cx| {
2309                        remove_wsl_distro(cx.entity(), index, distro_name.clone(), window, cx);
2310                        cx.focus_self(window);
2311                    }
2312                }))
2313                .child(
2314                    ListItem::new("remove-distro")
2315                        .toggle_state(entries[0].focus_handle.contains_focused(window, cx))
2316                        .inset(true)
2317                        .spacing(ui::ListItemSpacing::Sparse)
2318                        .start_slot(Icon::new(IconName::Trash).color(Color::Error))
2319                        .child(Label::new("Remove Distro").color(Color::Error))
2320                        .on_click(cx.listener(move |_, _, window, cx| {
2321                            remove_wsl_distro(cx.entity(), index, distro_name.clone(), window, cx);
2322                            cx.focus_self(window);
2323                        })),
2324                )
2325        })
2326    }
2327
2328    fn render_edit_ssh(
2329        &self,
2330        connection: &SshConnectionOptions,
2331        index: SshServerIndex,
2332        entries: &[NavigableEntry],
2333        window: &mut Window,
2334        cx: &mut Context<Self>,
2335    ) -> impl IntoElement {
2336        let connection_string = SharedString::new(connection.host.to_string());
2337
2338        v_flex()
2339            .child({
2340                let label = if connection.nickname.is_some() {
2341                    "Edit Nickname"
2342                } else {
2343                    "Add Nickname to Server"
2344                };
2345                div()
2346                    .id("ssh-options-add-nickname")
2347                    .track_focus(&entries[0].focus_handle)
2348                    .on_action(cx.listener(move |this, _: &menu::Confirm, window, cx| {
2349                        this.mode = Mode::EditNickname(EditNicknameState::new(index, window, cx));
2350                        cx.notify();
2351                    }))
2352                    .child(
2353                        ListItem::new("add-nickname")
2354                            .toggle_state(entries[0].focus_handle.contains_focused(window, cx))
2355                            .inset(true)
2356                            .spacing(ui::ListItemSpacing::Sparse)
2357                            .start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
2358                            .child(Label::new(label))
2359                            .on_click(cx.listener(move |this, _, window, cx| {
2360                                this.mode =
2361                                    Mode::EditNickname(EditNicknameState::new(index, window, cx));
2362                                cx.notify();
2363                            })),
2364                    )
2365            })
2366            .child({
2367                let workspace = self.workspace.clone();
2368                fn callback(
2369                    workspace: WeakEntity<Workspace>,
2370                    connection_string: SharedString,
2371                    cx: &mut App,
2372                ) {
2373                    cx.write_to_clipboard(ClipboardItem::new_string(connection_string.to_string()));
2374                    workspace
2375                        .update(cx, |this, cx| {
2376                            struct SshServerAddressCopiedToClipboard;
2377                            let notification = format!(
2378                                "Copied server address ({}) to clipboard",
2379                                connection_string
2380                            );
2381
2382                            this.show_toast(
2383                                Toast::new(
2384                                    NotificationId::composite::<SshServerAddressCopiedToClipboard>(
2385                                        connection_string.clone(),
2386                                    ),
2387                                    notification,
2388                                )
2389                                .autohide(),
2390                                cx,
2391                            );
2392                        })
2393                        .ok();
2394                }
2395                div()
2396                    .id("ssh-options-copy-server-address")
2397                    .track_focus(&entries[1].focus_handle)
2398                    .on_action({
2399                        let connection_string = connection_string.clone();
2400                        let workspace = self.workspace.clone();
2401                        move |_: &menu::Confirm, _, cx| {
2402                            callback(workspace.clone(), connection_string.clone(), cx);
2403                        }
2404                    })
2405                    .child(
2406                        ListItem::new("copy-server-address")
2407                            .toggle_state(entries[1].focus_handle.contains_focused(window, cx))
2408                            .inset(true)
2409                            .spacing(ui::ListItemSpacing::Sparse)
2410                            .start_slot(Icon::new(IconName::Copy).color(Color::Muted))
2411                            .child(Label::new("Copy Server Address"))
2412                            .end_hover_slot(
2413                                Label::new(connection_string.clone()).color(Color::Muted),
2414                            )
2415                            .on_click({
2416                                let connection_string = connection_string.clone();
2417                                move |_, _, cx| {
2418                                    callback(workspace.clone(), connection_string.clone(), cx);
2419                                }
2420                            }),
2421                    )
2422            })
2423            .child({
2424                fn remove_ssh_server(
2425                    remote_servers: Entity<RemoteServerProjects>,
2426                    index: SshServerIndex,
2427                    connection_string: SharedString,
2428                    window: &mut Window,
2429                    cx: &mut App,
2430                ) {
2431                    let prompt_message = format!("Remove server `{}`?", connection_string);
2432
2433                    let confirmation = window.prompt(
2434                        PromptLevel::Warning,
2435                        &prompt_message,
2436                        None,
2437                        &["Yes, remove it", "No, keep it"],
2438                        cx,
2439                    );
2440
2441                    cx.spawn(async move |cx| {
2442                        if confirmation.await.ok() == Some(0) {
2443                            remote_servers.update(cx, |this, cx| {
2444                                this.delete_ssh_server(index, cx);
2445                            });
2446                            remote_servers.update(cx, |this, cx| {
2447                                this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
2448                                cx.notify();
2449                            });
2450                        }
2451                        anyhow::Ok(())
2452                    })
2453                    .detach_and_log_err(cx);
2454                }
2455                div()
2456                    .id("ssh-options-copy-server-address")
2457                    .track_focus(&entries[2].focus_handle)
2458                    .on_action(cx.listener({
2459                        let connection_string = connection_string.clone();
2460                        move |_, _: &menu::Confirm, window, cx| {
2461                            remove_ssh_server(
2462                                cx.entity(),
2463                                index,
2464                                connection_string.clone(),
2465                                window,
2466                                cx,
2467                            );
2468                            cx.focus_self(window);
2469                        }
2470                    }))
2471                    .child(
2472                        ListItem::new("remove-server")
2473                            .toggle_state(entries[2].focus_handle.contains_focused(window, cx))
2474                            .inset(true)
2475                            .spacing(ui::ListItemSpacing::Sparse)
2476                            .start_slot(Icon::new(IconName::Trash).color(Color::Error))
2477                            .child(Label::new("Remove Server").color(Color::Error))
2478                            .on_click(cx.listener(move |_, _, window, cx| {
2479                                remove_ssh_server(
2480                                    cx.entity(),
2481                                    index,
2482                                    connection_string.clone(),
2483                                    window,
2484                                    cx,
2485                                );
2486                                cx.focus_self(window);
2487                            })),
2488                    )
2489            })
2490    }
2491
2492    fn render_edit_nickname(
2493        &self,
2494        state: &EditNicknameState,
2495        window: &mut Window,
2496        cx: &mut Context<Self>,
2497    ) -> impl IntoElement {
2498        let Some(connection) = RemoteSettings::get_global(cx)
2499            .ssh_connections()
2500            .nth(state.index.0)
2501        else {
2502            return v_flex()
2503                .id("ssh-edit-nickname")
2504                .track_focus(&self.focus_handle(cx));
2505        };
2506
2507        let connection_string = connection.host.clone();
2508        let nickname = connection.nickname.map(|s| s.into());
2509
2510        v_flex()
2511            .id("ssh-edit-nickname")
2512            .track_focus(&self.focus_handle(cx))
2513            .child(
2514                SshConnectionHeader {
2515                    connection_string: connection_string.into(),
2516                    paths: Default::default(),
2517                    nickname,
2518                    is_wsl: false,
2519                    is_devcontainer: false,
2520                }
2521                .render(window, cx),
2522            )
2523            .child(
2524                h_flex()
2525                    .p_2()
2526                    .border_t_1()
2527                    .border_color(cx.theme().colors().border_variant)
2528                    .child(state.editor.clone()),
2529            )
2530    }
2531
2532    fn render_default(
2533        &mut self,
2534        mut state: DefaultState,
2535        window: &mut Window,
2536        cx: &mut Context<Self>,
2537    ) -> impl IntoElement {
2538        let ssh_settings = RemoteSettings::get_global(cx);
2539        let mut should_rebuild = false;
2540
2541        let ssh_connections_changed = ssh_settings.ssh_connections.0.iter().ne(state
2542            .servers
2543            .iter()
2544            .filter_map(|server| match server {
2545                RemoteEntry::Project {
2546                    connection: Connection::Ssh(connection),
2547                    ..
2548                } => Some(connection),
2549                _ => None,
2550            }));
2551
2552        let wsl_connections_changed = ssh_settings.wsl_connections.0.iter().ne(state
2553            .servers
2554            .iter()
2555            .filter_map(|server| match server {
2556                RemoteEntry::Project {
2557                    connection: Connection::Wsl(connection),
2558                    ..
2559                } => Some(connection),
2560                _ => None,
2561            }));
2562
2563        if ssh_connections_changed || wsl_connections_changed {
2564            should_rebuild = true;
2565        };
2566
2567        if !should_rebuild && ssh_settings.read_ssh_config {
2568            let current_ssh_hosts: BTreeSet<SharedString> = state
2569                .servers
2570                .iter()
2571                .filter_map(|server| match server {
2572                    RemoteEntry::SshConfig { host, .. } => Some(host.clone()),
2573                    _ => None,
2574                })
2575                .collect();
2576            let mut expected_ssh_hosts = self.ssh_config_servers.clone();
2577            for server in &state.servers {
2578                if let RemoteEntry::Project {
2579                    connection: Connection::Ssh(connection),
2580                    ..
2581                } = server
2582                {
2583                    expected_ssh_hosts.remove(connection.host.as_str());
2584                }
2585            }
2586            should_rebuild = current_ssh_hosts != expected_ssh_hosts;
2587        }
2588
2589        if should_rebuild {
2590            self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
2591            if let Mode::Default(new_state) = &self.mode {
2592                state = new_state.clone();
2593            }
2594        }
2595
2596        let connect_button = div()
2597            .id("ssh-connect-new-server-container")
2598            .track_focus(&state.add_new_server.focus_handle)
2599            .anchor_scroll(state.add_new_server.scroll_anchor.clone())
2600            .child(
2601                ListItem::new("register-remote-server-button")
2602                    .toggle_state(
2603                        state
2604                            .add_new_server
2605                            .focus_handle
2606                            .contains_focused(window, cx),
2607                    )
2608                    .inset(true)
2609                    .spacing(ui::ListItemSpacing::Sparse)
2610                    .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
2611                    .child(Label::new("Connect SSH Server"))
2612                    .on_click(cx.listener(|this, _, window, cx| {
2613                        let state = CreateRemoteServer::new(window, cx);
2614                        this.mode = Mode::CreateRemoteServer(state);
2615
2616                        cx.notify();
2617                    })),
2618            )
2619            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
2620                let state = CreateRemoteServer::new(window, cx);
2621                this.mode = Mode::CreateRemoteServer(state);
2622
2623                cx.notify();
2624            }));
2625
2626        let connect_dev_container_button = div()
2627            .id("connect-new-dev-container")
2628            .track_focus(&state.add_new_devcontainer.focus_handle)
2629            .anchor_scroll(state.add_new_devcontainer.scroll_anchor.clone())
2630            .child(
2631                ListItem::new("register-dev-container-button")
2632                    .toggle_state(
2633                        state
2634                            .add_new_devcontainer
2635                            .focus_handle
2636                            .contains_focused(window, cx),
2637                    )
2638                    .inset(true)
2639                    .spacing(ui::ListItemSpacing::Sparse)
2640                    .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
2641                    .child(Label::new("Connect Dev Container"))
2642                    .on_click(cx.listener(|this, _, window, cx| {
2643                        this.init_dev_container_mode(window, cx);
2644                    })),
2645            )
2646            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
2647                this.init_dev_container_mode(window, cx);
2648            }));
2649
2650        #[cfg(target_os = "windows")]
2651        let wsl_connect_button = div()
2652            .id("wsl-connect-new-server")
2653            .track_focus(&state.add_new_wsl.focus_handle)
2654            .anchor_scroll(state.add_new_wsl.scroll_anchor.clone())
2655            .child(
2656                ListItem::new("wsl-add-new-server")
2657                    .toggle_state(state.add_new_wsl.focus_handle.contains_focused(window, cx))
2658                    .inset(true)
2659                    .spacing(ui::ListItemSpacing::Sparse)
2660                    .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
2661                    .child(Label::new("Add WSL Distro"))
2662                    .on_click(cx.listener(|this, _, window, cx| {
2663                        let state = AddWslDistro::new(window, cx);
2664                        this.mode = Mode::AddWslDistro(state);
2665
2666                        cx.notify();
2667                    })),
2668            )
2669            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
2670                let state = AddWslDistro::new(window, cx);
2671                this.mode = Mode::AddWslDistro(state);
2672
2673                cx.notify();
2674            }));
2675
2676        let has_open_project = self
2677            .workspace
2678            .upgrade()
2679            .map(|workspace| {
2680                workspace
2681                    .read(cx)
2682                    .project()
2683                    .read(cx)
2684                    .visible_worktrees(cx)
2685                    .next()
2686                    .is_some()
2687            })
2688            .unwrap_or(false);
2689
2690        // We cannot currently connect a dev container from within a remote server due to the remote_server architecture
2691        let is_local = self
2692            .workspace
2693            .upgrade()
2694            .map(|workspace| workspace.read(cx).project().read(cx).is_local())
2695            .unwrap_or(true);
2696
2697        let modal_section = v_flex()
2698            .track_focus(&self.focus_handle(cx))
2699            .id("ssh-server-list")
2700            .overflow_y_scroll()
2701            .track_scroll(&state.scroll_handle)
2702            .size_full()
2703            .child(connect_button)
2704            .when(has_open_project && is_local, |this| {
2705                this.child(connect_dev_container_button)
2706            });
2707
2708        #[cfg(target_os = "windows")]
2709        let modal_section = modal_section.child(wsl_connect_button);
2710        #[cfg(not(target_os = "windows"))]
2711        let modal_section = modal_section;
2712
2713        let mut modal_section = Navigable::new(
2714            modal_section
2715                .child(
2716                    List::new()
2717                        .empty_message(
2718                            h_flex()
2719                                .size_full()
2720                                .p_2()
2721                                .justify_center()
2722                                .border_t_1()
2723                                .border_color(cx.theme().colors().border_variant)
2724                                .child(
2725                                    Label::new("No remote servers registered yet.")
2726                                        .color(Color::Muted),
2727                                )
2728                                .into_any_element(),
2729                        )
2730                        .children(state.servers.iter().enumerate().map(|(ix, connection)| {
2731                            self.render_remote_connection(ix, connection.clone(), window, cx)
2732                                .into_any_element()
2733                        })),
2734                )
2735                .into_any_element(),
2736        )
2737        .entry(state.add_new_server.clone());
2738
2739        if has_open_project && is_local {
2740            modal_section = modal_section.entry(state.add_new_devcontainer.clone());
2741        }
2742
2743        if cfg!(target_os = "windows") {
2744            modal_section = modal_section.entry(state.add_new_wsl.clone());
2745        }
2746
2747        for server in &state.servers {
2748            match server {
2749                RemoteEntry::Project {
2750                    open_folder,
2751                    projects,
2752                    configure,
2753                    ..
2754                } => {
2755                    for (navigation_state, _) in projects {
2756                        modal_section = modal_section.entry(navigation_state.clone());
2757                    }
2758                    modal_section = modal_section
2759                        .entry(open_folder.clone())
2760                        .entry(configure.clone());
2761                }
2762                RemoteEntry::SshConfig { open_folder, .. } => {
2763                    modal_section = modal_section.entry(open_folder.clone());
2764                }
2765            }
2766        }
2767        let mut modal_section = modal_section.render(window, cx).into_any_element();
2768
2769        let is_project_selected = state.servers.iter().any(|server| match server {
2770            RemoteEntry::Project { projects, .. } => projects
2771                .iter()
2772                .any(|(entry, _)| entry.focus_handle.contains_focused(window, cx)),
2773            RemoteEntry::SshConfig { .. } => false,
2774        });
2775
2776        Modal::new("remote-projects", None)
2777            .header(ModalHeader::new().headline("Remote Projects"))
2778            .section(
2779                Section::new().padded(false).child(
2780                    v_flex()
2781                        .min_h(rems(20.))
2782                        .size_full()
2783                        .relative()
2784                        .child(ListSeparator)
2785                        .child(
2786                            canvas(
2787                                |bounds, window, cx| {
2788                                    modal_section.prepaint_as_root(
2789                                        bounds.origin,
2790                                        bounds.size.into(),
2791                                        window,
2792                                        cx,
2793                                    );
2794                                    modal_section
2795                                },
2796                                |_, mut modal_section, window, cx| {
2797                                    modal_section.paint(window, cx);
2798                                },
2799                            )
2800                            .size_full(),
2801                        )
2802                        .vertical_scrollbar_for(&state.scroll_handle, window, cx),
2803                ),
2804            )
2805            .footer(ModalFooter::new().end_slot({
2806                let confirm_button = |label: SharedString| {
2807                    Button::new("select", label)
2808                        .key_binding(KeyBinding::for_action(&menu::Confirm, cx))
2809                        .on_click(|_, window, cx| {
2810                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)
2811                        })
2812                };
2813
2814                if is_project_selected {
2815                    h_flex()
2816                        .gap_1()
2817                        .child(
2818                            Button::new("open_new_window", "New Window")
2819                                .key_binding(KeyBinding::for_action(&menu::SecondaryConfirm, cx))
2820                                .on_click(|_, window, cx| {
2821                                    window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
2822                                }),
2823                        )
2824                        .child(confirm_button("Open".into()))
2825                        .into_any_element()
2826                } else {
2827                    confirm_button("Select".into()).into_any_element()
2828                }
2829            }))
2830            .into_any_element()
2831    }
2832
2833    fn create_host_from_ssh_config(
2834        &mut self,
2835        ssh_config_host: &SharedString,
2836        cx: &mut Context<'_, Self>,
2837    ) -> SshServerIndex {
2838        let new_ix = Arc::new(AtomicUsize::new(0));
2839
2840        let update_new_ix = new_ix.clone();
2841        self.update_settings_file(cx, move |settings, _| {
2842            update_new_ix.store(
2843                settings
2844                    .ssh_connections
2845                    .as_ref()
2846                    .map_or(0, |connections| connections.len()),
2847                atomic::Ordering::Release,
2848            );
2849        });
2850
2851        self.add_ssh_server(
2852            SshConnectionOptions {
2853                host: ssh_config_host.to_string().into(),
2854                ..SshConnectionOptions::default()
2855            },
2856            cx,
2857        );
2858        self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
2859        SshServerIndex(new_ix.load(atomic::Ordering::Acquire))
2860    }
2861}
2862
2863fn spawn_ssh_config_watch(fs: Arc<dyn Fs>, cx: &Context<RemoteServerProjects>) -> Task<()> {
2864    enum ConfigSource {
2865        User(String),
2866        Global(String),
2867    }
2868
2869    let mut streams = Vec::new();
2870    let mut tasks = Vec::new();
2871
2872    // Setup User Watcher
2873    let user_path = user_ssh_config_file();
2874    info!("SSH: Watching User Config at: {:?}", user_path);
2875
2876    // We clone 'fs' here because we might need it again for the global watcher.
2877    let (user_s, user_t) = watch_config_file(cx.background_executor(), fs.clone(), user_path);
2878    streams.push(user_s.map(ConfigSource::User).boxed());
2879    tasks.push(user_t);
2880
2881    // Setup Global Watcher
2882    if let Some(gp) = global_ssh_config_file() {
2883        info!("SSH: Watching Global Config at: {:?}", gp);
2884        let (global_s, global_t) =
2885            watch_config_file(cx.background_executor(), fs, gp.to_path_buf());
2886        streams.push(global_s.map(ConfigSource::Global).boxed());
2887        tasks.push(global_t);
2888    } else {
2889        debug!("SSH: No Global Config defined.");
2890    }
2891
2892    // Combine into a single stream so that only one is parsed at once.
2893    let mut merged_stream = futures::stream::select_all(streams);
2894
2895    cx.spawn(async move |remote_server_projects, cx| {
2896        let _tasks = tasks; // Keeps the background watchers alive
2897        let mut global_hosts = BTreeSet::default();
2898        let mut user_hosts = BTreeSet::default();
2899
2900        while let Some(event) = merged_stream.next().await {
2901            match event {
2902                ConfigSource::Global(content) => {
2903                    global_hosts = parse_ssh_config_hosts(&content);
2904                }
2905                ConfigSource::User(content) => {
2906                    user_hosts = parse_ssh_config_hosts(&content);
2907                }
2908            }
2909
2910            // Sync to Model
2911            if remote_server_projects
2912                .update(cx, |project, cx| {
2913                    project.ssh_config_servers = global_hosts
2914                        .iter()
2915                        .chain(user_hosts.iter())
2916                        .map(SharedString::from)
2917                        .collect();
2918                    cx.notify();
2919                })
2920                .is_err()
2921            {
2922                return;
2923            }
2924        }
2925    })
2926}
2927
2928fn get_text(element: &Entity<Editor>, cx: &mut App) -> String {
2929    element.read(cx).text(cx).trim().to_string()
2930}
2931
2932impl ModalView for RemoteServerProjects {}
2933
2934impl Focusable for RemoteServerProjects {
2935    fn focus_handle(&self, cx: &App) -> FocusHandle {
2936        match &self.mode {
2937            Mode::ProjectPicker(picker) => picker.focus_handle(cx),
2938            _ => self.focus_handle.clone(),
2939        }
2940    }
2941}
2942
2943impl EventEmitter<DismissEvent> for RemoteServerProjects {}
2944
2945impl Render for RemoteServerProjects {
2946    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2947        div()
2948            .elevation_3(cx)
2949            .w(rems(34.))
2950            .key_context("RemoteServerModal")
2951            .on_action(cx.listener(Self::cancel))
2952            .on_action(cx.listener(Self::confirm))
2953            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
2954                this.focus_handle(cx).focus(window, cx);
2955            }))
2956            .on_mouse_down_out(cx.listener(|this, _, _, cx| {
2957                if matches!(this.mode, Mode::Default(_)) {
2958                    cx.emit(DismissEvent)
2959                }
2960            }))
2961            .child(match &self.mode {
2962                Mode::Default(state) => self
2963                    .render_default(state.clone(), window, cx)
2964                    .into_any_element(),
2965                Mode::ViewServerOptions(state) => self
2966                    .render_view_options(state.clone(), window, cx)
2967                    .into_any_element(),
2968                Mode::ProjectPicker(element) => element.clone().into_any_element(),
2969                Mode::CreateRemoteServer(state) => self
2970                    .render_create_remote_server(state, window, cx)
2971                    .into_any_element(),
2972                Mode::CreateRemoteDevContainer(state) => self
2973                    .render_create_dev_container(state, window, cx)
2974                    .into_any_element(),
2975                Mode::EditNickname(state) => self
2976                    .render_edit_nickname(state, window, cx)
2977                    .into_any_element(),
2978                #[cfg(target_os = "windows")]
2979                Mode::AddWslDistro(state) => self
2980                    .render_add_wsl_distro(state, window, cx)
2981                    .into_any_element(),
2982            })
2983    }
2984}