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