remote_servers.rs

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