remote_servers.rs

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