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