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                                true,
1004                                cx,
1005                            ),
1006                        )
1007                    })?;
1008
1009                    let home_dir = project
1010                        .read_with(cx, |project, cx| project.resolve_abs_path("~", cx))?
1011                        .await
1012                        .and_then(|path| path.into_abs_path())
1013                        .map(|path| RemotePathBuf::new(path, path_style))
1014                        .unwrap_or_else(|| match path_style {
1015                            PathStyle::Posix => RemotePathBuf::from_str("/", PathStyle::Posix),
1016                            PathStyle::Windows => {
1017                                RemotePathBuf::from_str("C:\\", PathStyle::Windows)
1018                            }
1019                        });
1020
1021                    workspace
1022                        .update_in(cx, |workspace, window, cx| {
1023                            let weak = cx.entity().downgrade();
1024                            workspace.toggle_modal(window, cx, |window, cx| {
1025                                RemoteServerProjects::project_picker(
1026                                    create_new_window,
1027                                    index,
1028                                    connection_options,
1029                                    project,
1030                                    home_dir,
1031                                    window,
1032                                    cx,
1033                                    weak,
1034                                )
1035                            });
1036                        })
1037                        .ok();
1038                    Ok(())
1039                })
1040                .detach();
1041            })
1042        })
1043    }
1044
1045    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
1046        match &self.mode {
1047            Mode::Default(_) | Mode::ViewServerOptions(_) => {}
1048            Mode::ProjectPicker(_) => {}
1049            Mode::CreateRemoteServer(state) => {
1050                if let Some(prompt) = state.ssh_prompt.as_ref() {
1051                    prompt.update(cx, |prompt, cx| {
1052                        prompt.confirm(window, cx);
1053                    });
1054                    return;
1055                }
1056
1057                self.create_ssh_server(state.address_editor.clone(), window, cx);
1058            }
1059            Mode::CreateRemoteDevContainer(_) => {}
1060            Mode::EditNickname(state) => {
1061                let text = Some(state.editor.read(cx).text(cx)).filter(|text| !text.is_empty());
1062                let index = state.index;
1063                self.update_settings_file(cx, move |setting, _| {
1064                    if let Some(connections) = setting.ssh_connections.as_mut()
1065                        && let Some(connection) = connections.get_mut(index.0)
1066                    {
1067                        connection.nickname = text;
1068                    }
1069                });
1070                self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
1071                self.focus_handle.focus(window);
1072            }
1073            #[cfg(target_os = "windows")]
1074            Mode::AddWslDistro(state) => {
1075                let delegate = &state.picker.read(cx).delegate;
1076                let distro = delegate.selected_distro().unwrap();
1077                self.connect_wsl_distro(state.picker.clone(), distro, window, cx);
1078            }
1079        }
1080    }
1081
1082    fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
1083        match &self.mode {
1084            Mode::Default(_) => cx.emit(DismissEvent),
1085            Mode::CreateRemoteServer(state) if state.ssh_prompt.is_some() => {
1086                let new_state = CreateRemoteServer::new(window, cx);
1087                let old_prompt = state.address_editor.read(cx).text(cx);
1088                new_state.address_editor.update(cx, |this, cx| {
1089                    this.set_text(old_prompt, window, cx);
1090                });
1091
1092                self.mode = Mode::CreateRemoteServer(new_state);
1093                cx.notify();
1094            }
1095            _ => {
1096                self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
1097                self.focus_handle(cx).focus(window);
1098                cx.notify();
1099            }
1100        }
1101    }
1102
1103    fn render_remote_connection(
1104        &mut self,
1105        ix: usize,
1106        remote_server: RemoteEntry,
1107        window: &mut Window,
1108        cx: &mut Context<Self>,
1109    ) -> impl IntoElement {
1110        let connection = remote_server.connection().into_owned();
1111
1112        let (main_label, aux_label, is_wsl) = match &connection {
1113            Connection::Ssh(connection) => {
1114                if let Some(nickname) = connection.nickname.clone() {
1115                    let aux_label = SharedString::from(format!("({})", connection.host));
1116                    (nickname.into(), Some(aux_label), false)
1117                } else {
1118                    (connection.host.clone(), None, false)
1119                }
1120            }
1121            Connection::Wsl(wsl_connection_options) => {
1122                (wsl_connection_options.distro_name.clone(), None, true)
1123            }
1124            Connection::DevContainer(dev_container_options) => {
1125                (dev_container_options.name.clone(), None, false)
1126            }
1127        };
1128        v_flex()
1129            .w_full()
1130            .child(ListSeparator)
1131            .child(
1132                h_flex()
1133                    .group("ssh-server")
1134                    .w_full()
1135                    .pt_0p5()
1136                    .px_3()
1137                    .gap_1()
1138                    .overflow_hidden()
1139                    .child(
1140                        h_flex()
1141                            .gap_1()
1142                            .max_w_96()
1143                            .overflow_hidden()
1144                            .text_ellipsis()
1145                            .when(is_wsl, |this| {
1146                                this.child(
1147                                    Label::new("WSL:")
1148                                        .size(LabelSize::Small)
1149                                        .color(Color::Muted),
1150                                )
1151                            })
1152                            .child(
1153                                Label::new(main_label)
1154                                    .size(LabelSize::Small)
1155                                    .color(Color::Muted),
1156                            ),
1157                    )
1158                    .children(
1159                        aux_label.map(|label| {
1160                            Label::new(label).size(LabelSize::Small).color(Color::Muted)
1161                        }),
1162                    ),
1163            )
1164            .child(match &remote_server {
1165                RemoteEntry::Project {
1166                    open_folder,
1167                    projects,
1168                    configure,
1169                    connection,
1170                    index,
1171                } => {
1172                    let index = *index;
1173                    List::new()
1174                        .empty_message("No projects.")
1175                        .children(projects.iter().enumerate().map(|(pix, p)| {
1176                            v_flex().gap_0p5().child(self.render_remote_project(
1177                                index,
1178                                remote_server.clone(),
1179                                pix,
1180                                p,
1181                                window,
1182                                cx,
1183                            ))
1184                        }))
1185                        .child(
1186                            h_flex()
1187                                .id(("new-remote-project-container", ix))
1188                                .track_focus(&open_folder.focus_handle)
1189                                .anchor_scroll(open_folder.scroll_anchor.clone())
1190                                .on_action(cx.listener({
1191                                    let connection = connection.clone();
1192                                    move |this, _: &menu::Confirm, window, cx| {
1193                                        this.create_remote_project(
1194                                            index,
1195                                            connection.clone().into(),
1196                                            window,
1197                                            cx,
1198                                        );
1199                                    }
1200                                }))
1201                                .child(
1202                                    ListItem::new(("new-remote-project", ix))
1203                                        .toggle_state(
1204                                            open_folder.focus_handle.contains_focused(window, cx),
1205                                        )
1206                                        .inset(true)
1207                                        .spacing(ui::ListItemSpacing::Sparse)
1208                                        .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
1209                                        .child(Label::new("Open Folder"))
1210                                        .on_click(cx.listener({
1211                                            let connection = connection.clone();
1212                                            move |this, _, window, cx| {
1213                                                this.create_remote_project(
1214                                                    index,
1215                                                    connection.clone().into(),
1216                                                    window,
1217                                                    cx,
1218                                                );
1219                                            }
1220                                        })),
1221                                ),
1222                        )
1223                        .child(
1224                            h_flex()
1225                                .id(("server-options-container", ix))
1226                                .track_focus(&configure.focus_handle)
1227                                .anchor_scroll(configure.scroll_anchor.clone())
1228                                .on_action(cx.listener({
1229                                    let connection = connection.clone();
1230                                    move |this, _: &menu::Confirm, window, cx| {
1231                                        this.view_server_options(
1232                                            (index, connection.clone().into()),
1233                                            window,
1234                                            cx,
1235                                        );
1236                                    }
1237                                }))
1238                                .child(
1239                                    ListItem::new(("server-options", ix))
1240                                        .toggle_state(
1241                                            configure.focus_handle.contains_focused(window, cx),
1242                                        )
1243                                        .inset(true)
1244                                        .spacing(ui::ListItemSpacing::Sparse)
1245                                        .start_slot(
1246                                            Icon::new(IconName::Settings).color(Color::Muted),
1247                                        )
1248                                        .child(Label::new("View Server Options"))
1249                                        .on_click(cx.listener({
1250                                            let ssh_connection = connection.clone();
1251                                            move |this, _, window, cx| {
1252                                                this.view_server_options(
1253                                                    (index, ssh_connection.clone().into()),
1254                                                    window,
1255                                                    cx,
1256                                                );
1257                                            }
1258                                        })),
1259                                ),
1260                        )
1261                }
1262                RemoteEntry::SshConfig { open_folder, host } => List::new().child(
1263                    h_flex()
1264                        .id(("new-remote-project-container", ix))
1265                        .track_focus(&open_folder.focus_handle)
1266                        .anchor_scroll(open_folder.scroll_anchor.clone())
1267                        .on_action(cx.listener({
1268                            let connection = connection.clone();
1269                            let host = host.clone();
1270                            move |this, _: &menu::Confirm, window, cx| {
1271                                let new_ix = this.create_host_from_ssh_config(&host, cx);
1272                                this.create_remote_project(
1273                                    new_ix.into(),
1274                                    connection.clone().into(),
1275                                    window,
1276                                    cx,
1277                                );
1278                            }
1279                        }))
1280                        .child(
1281                            ListItem::new(("new-remote-project", ix))
1282                                .toggle_state(open_folder.focus_handle.contains_focused(window, cx))
1283                                .inset(true)
1284                                .spacing(ui::ListItemSpacing::Sparse)
1285                                .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
1286                                .child(Label::new("Open Folder"))
1287                                .on_click(cx.listener({
1288                                    let host = host.clone();
1289                                    move |this, _, window, cx| {
1290                                        let new_ix = this.create_host_from_ssh_config(&host, cx);
1291                                        this.create_remote_project(
1292                                            new_ix.into(),
1293                                            connection.clone().into(),
1294                                            window,
1295                                            cx,
1296                                        );
1297                                    }
1298                                })),
1299                        ),
1300                ),
1301            })
1302    }
1303
1304    fn render_remote_project(
1305        &mut self,
1306        server_ix: ServerIndex,
1307        server: RemoteEntry,
1308        ix: usize,
1309        (navigation, project): &(NavigableEntry, RemoteProject),
1310        window: &mut Window,
1311        cx: &mut Context<Self>,
1312    ) -> impl IntoElement {
1313        let create_new_window = self.create_new_window;
1314        let is_from_zed = server.is_from_zed();
1315        let element_id_base = SharedString::from(format!(
1316            "remote-project-{}",
1317            match server_ix {
1318                ServerIndex::Ssh(index) => format!("ssh-{index}"),
1319                ServerIndex::Wsl(index) => format!("wsl-{index}"),
1320            }
1321        ));
1322        let container_element_id_base =
1323            SharedString::from(format!("remote-project-container-{element_id_base}"));
1324
1325        let callback = Rc::new({
1326            let project = project.clone();
1327            move |remote_server_projects: &mut Self,
1328                  secondary_confirm: bool,
1329                  window: &mut Window,
1330                  cx: &mut Context<Self>| {
1331                let Some(app_state) = remote_server_projects
1332                    .workspace
1333                    .read_with(cx, |workspace, _| workspace.app_state().clone())
1334                    .log_err()
1335                else {
1336                    return;
1337                };
1338                let project = project.clone();
1339                let server = server.connection().into_owned();
1340                cx.emit(DismissEvent);
1341
1342                let replace_window = match (create_new_window, secondary_confirm) {
1343                    (true, false) | (false, true) => None,
1344                    (true, true) | (false, false) => window.window_handle().downcast::<Workspace>(),
1345                };
1346
1347                cx.spawn_in(window, async move |_, cx| {
1348                    let result = open_remote_project(
1349                        server.into(),
1350                        project.paths.into_iter().map(PathBuf::from).collect(),
1351                        app_state,
1352                        OpenOptions {
1353                            replace_window,
1354                            ..OpenOptions::default()
1355                        },
1356                        cx,
1357                    )
1358                    .await;
1359                    if let Err(e) = result {
1360                        log::error!("Failed to connect: {e:#}");
1361                        cx.prompt(
1362                            gpui::PromptLevel::Critical,
1363                            "Failed to connect",
1364                            Some(&e.to_string()),
1365                            &["Ok"],
1366                        )
1367                        .await
1368                        .ok();
1369                    }
1370                })
1371                .detach();
1372            }
1373        });
1374
1375        div()
1376            .id((container_element_id_base, ix))
1377            .track_focus(&navigation.focus_handle)
1378            .anchor_scroll(navigation.scroll_anchor.clone())
1379            .on_action(cx.listener({
1380                let callback = callback.clone();
1381                move |this, _: &menu::Confirm, window, cx| {
1382                    callback(this, false, window, cx);
1383                }
1384            }))
1385            .on_action(cx.listener({
1386                let callback = callback.clone();
1387                move |this, _: &menu::SecondaryConfirm, window, cx| {
1388                    callback(this, true, window, cx);
1389                }
1390            }))
1391            .child(
1392                ListItem::new((element_id_base, ix))
1393                    .toggle_state(navigation.focus_handle.contains_focused(window, cx))
1394                    .inset(true)
1395                    .spacing(ui::ListItemSpacing::Sparse)
1396                    .start_slot(
1397                        Icon::new(IconName::Folder)
1398                            .color(Color::Muted)
1399                            .size(IconSize::Small),
1400                    )
1401                    .child(Label::new(project.paths.join(", ")))
1402                    .on_click(cx.listener(move |this, e: &ClickEvent, window, cx| {
1403                        let secondary_confirm = e.modifiers().platform;
1404                        callback(this, secondary_confirm, window, cx)
1405                    }))
1406                    .when(is_from_zed, |server_list_item| {
1407                        server_list_item.end_hover_slot::<AnyElement>(Some(
1408                            div()
1409                                .mr_2()
1410                                .child({
1411                                    let project = project.clone();
1412                                    // Right-margin to offset it from the Scrollbar
1413                                    IconButton::new("remove-remote-project", IconName::Trash)
1414                                        .icon_size(IconSize::Small)
1415                                        .shape(IconButtonShape::Square)
1416                                        .size(ButtonSize::Large)
1417                                        .tooltip(Tooltip::text("Delete Remote Project"))
1418                                        .on_click(cx.listener(move |this, _, _, cx| {
1419                                            this.delete_remote_project(server_ix, &project, cx)
1420                                        }))
1421                                })
1422                                .into_any_element(),
1423                        ))
1424                    }),
1425            )
1426    }
1427
1428    fn update_settings_file(
1429        &mut self,
1430        cx: &mut Context<Self>,
1431        f: impl FnOnce(&mut RemoteSettingsContent, &App) + Send + Sync + 'static,
1432    ) {
1433        let Some(fs) = self
1434            .workspace
1435            .read_with(cx, |workspace, _| workspace.app_state().fs.clone())
1436            .log_err()
1437        else {
1438            return;
1439        };
1440        update_settings_file(fs, cx, move |setting, cx| f(&mut setting.remote, cx));
1441    }
1442
1443    fn delete_ssh_server(&mut self, server: SshServerIndex, cx: &mut Context<Self>) {
1444        self.update_settings_file(cx, move |setting, _| {
1445            if let Some(connections) = setting.ssh_connections.as_mut() {
1446                connections.remove(server.0);
1447            }
1448        });
1449    }
1450
1451    fn delete_remote_project(
1452        &mut self,
1453        server: ServerIndex,
1454        project: &RemoteProject,
1455        cx: &mut Context<Self>,
1456    ) {
1457        match server {
1458            ServerIndex::Ssh(server) => {
1459                self.delete_ssh_project(server, project, cx);
1460            }
1461            ServerIndex::Wsl(server) => {
1462                self.delete_wsl_project(server, project, cx);
1463            }
1464        }
1465    }
1466
1467    fn delete_ssh_project(
1468        &mut self,
1469        server: SshServerIndex,
1470        project: &RemoteProject,
1471        cx: &mut Context<Self>,
1472    ) {
1473        let project = project.clone();
1474        self.update_settings_file(cx, move |setting, _| {
1475            if let Some(server) = setting
1476                .ssh_connections
1477                .as_mut()
1478                .and_then(|connections| connections.get_mut(server.0))
1479            {
1480                server.projects.remove(&project);
1481            }
1482        });
1483    }
1484
1485    fn delete_wsl_project(
1486        &mut self,
1487        server: WslServerIndex,
1488        project: &RemoteProject,
1489        cx: &mut Context<Self>,
1490    ) {
1491        let project = project.clone();
1492        self.update_settings_file(cx, move |setting, _| {
1493            if let Some(server) = setting
1494                .wsl_connections
1495                .as_mut()
1496                .and_then(|connections| connections.get_mut(server.0))
1497            {
1498                server.projects.remove(&project);
1499            }
1500        });
1501    }
1502
1503    fn delete_wsl_distro(&mut self, server: WslServerIndex, cx: &mut Context<Self>) {
1504        self.update_settings_file(cx, move |setting, _| {
1505            if let Some(connections) = setting.wsl_connections.as_mut() {
1506                connections.remove(server.0);
1507            }
1508        });
1509    }
1510
1511    fn add_ssh_server(
1512        &mut self,
1513        connection_options: remote::SshConnectionOptions,
1514        cx: &mut Context<Self>,
1515    ) {
1516        self.update_settings_file(cx, move |setting, _| {
1517            setting
1518                .ssh_connections
1519                .get_or_insert(Default::default())
1520                .push(SshConnection {
1521                    host: SharedString::from(connection_options.host),
1522                    username: connection_options.username,
1523                    port: connection_options.port,
1524                    projects: BTreeSet::new(),
1525                    nickname: None,
1526                    args: connection_options.args.unwrap_or_default(),
1527                    upload_binary_over_ssh: None,
1528                    port_forwards: connection_options.port_forwards,
1529                    connection_timeout: connection_options.connection_timeout,
1530                })
1531        });
1532    }
1533
1534    fn edit_in_dev_container_json(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1535        let Some(workspace) = self.workspace.upgrade() else {
1536            cx.emit(DismissEvent);
1537            cx.notify();
1538            return;
1539        };
1540
1541        workspace.update(cx, |workspace, cx| {
1542            let project = workspace.project().clone();
1543
1544            let worktree = project
1545                .read(cx)
1546                .visible_worktrees(cx)
1547                .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
1548
1549            if let Some(worktree) = worktree {
1550                let tree_id = worktree.read(cx).id();
1551                let devcontainer_path = RelPath::unix(".devcontainer/devcontainer.json").unwrap();
1552                cx.spawn_in(window, async move |workspace, cx| {
1553                    workspace
1554                        .update_in(cx, |workspace, window, cx| {
1555                            workspace.open_path(
1556                                (tree_id, devcontainer_path),
1557                                None,
1558                                true,
1559                                window,
1560                                cx,
1561                            )
1562                        })?
1563                        .await
1564                })
1565                .detach();
1566            } else {
1567                return;
1568            }
1569        });
1570        cx.emit(DismissEvent);
1571        cx.notify();
1572    }
1573
1574    fn open_dev_container(&self, window: &mut Window, cx: &mut Context<Self>) {
1575        let Some(app_state) = self
1576            .workspace
1577            .read_with(cx, |workspace, _| workspace.app_state().clone())
1578            .log_err()
1579        else {
1580            return;
1581        };
1582
1583        let replace_window = window.window_handle().downcast::<Workspace>();
1584
1585        cx.spawn_in(window, async move |entity, cx| {
1586            let (connection, starting_dir) =
1587                match start_dev_container(cx, app_state.node_runtime.clone()).await {
1588                    Ok((c, s)) => (c, s),
1589                    Err(e) => {
1590                        log::error!("Failed to start dev container: {:?}", e);
1591                        entity
1592                            .update_in(cx, |remote_server_projects, window, cx| {
1593                                remote_server_projects.mode = Mode::CreateRemoteDevContainer(
1594                                    CreateRemoteDevContainer::new(window, cx).progress(
1595                                        DevContainerCreationProgress::Error(format!("{:?}", e)),
1596                                    ),
1597                                );
1598                            })
1599                            .log_err();
1600                        return;
1601                    }
1602                };
1603            entity
1604                .update(cx, |_, cx| {
1605                    cx.emit(DismissEvent);
1606                })
1607                .log_err();
1608
1609            let result = open_remote_project(
1610                connection.into(),
1611                vec![starting_dir].into_iter().map(PathBuf::from).collect(),
1612                app_state,
1613                OpenOptions {
1614                    replace_window,
1615                    ..OpenOptions::default()
1616                },
1617                cx,
1618            )
1619            .await;
1620            if let Err(e) = result {
1621                log::error!("Failed to connect: {e:#}");
1622                cx.prompt(
1623                    gpui::PromptLevel::Critical,
1624                    "Failed to connect",
1625                    Some(&e.to_string()),
1626                    &["Ok"],
1627                )
1628                .await
1629                .ok();
1630            }
1631        })
1632        .detach();
1633    }
1634
1635    fn render_create_dev_container(
1636        &self,
1637        state: &CreateRemoteDevContainer,
1638        window: &mut Window,
1639        cx: &mut Context<Self>,
1640    ) -> impl IntoElement {
1641        match &state.progress {
1642            DevContainerCreationProgress::Error(message) => {
1643                self.focus_handle(cx).focus(window);
1644                return div()
1645                    .track_focus(&self.focus_handle(cx))
1646                    .size_full()
1647                    .child(
1648                        v_flex()
1649                            .py_1()
1650                            .child(
1651                                ListItem::new("Error")
1652                                    .inset(true)
1653                                    .selectable(false)
1654                                    .spacing(ui::ListItemSpacing::Sparse)
1655                                    .start_slot(Icon::new(IconName::XCircle).color(Color::Error))
1656                                    .child(Label::new("Error Creating Dev Container:"))
1657                                    .child(Label::new(message).buffer_font(cx)),
1658                            )
1659                            .child(ListSeparator)
1660                            .child(
1661                                div()
1662                                    .id("devcontainer-go-back")
1663                                    .track_focus(&state.entries[0].focus_handle)
1664                                    .on_action(cx.listener(
1665                                        |this, _: &menu::Confirm, window, cx| {
1666                                            this.mode =
1667                                                Mode::default_mode(&this.ssh_config_servers, cx);
1668                                            cx.focus_self(window);
1669                                            cx.notify();
1670                                        },
1671                                    ))
1672                                    .child(
1673                                        ListItem::new("li-devcontainer-go-back")
1674                                            .toggle_state(
1675                                                state.entries[0]
1676                                                    .focus_handle
1677                                                    .contains_focused(window, cx),
1678                                            )
1679                                            .inset(true)
1680                                            .spacing(ui::ListItemSpacing::Sparse)
1681                                            .start_slot(
1682                                                Icon::new(IconName::ArrowLeft).color(Color::Muted),
1683                                            )
1684                                            .child(Label::new("Go Back"))
1685                                            .end_slot(
1686                                                KeyBinding::for_action_in(
1687                                                    &menu::Cancel,
1688                                                    &self.focus_handle,
1689                                                    cx,
1690                                                )
1691                                                .size(rems_from_px(12.)),
1692                                            )
1693                                            .on_click(cx.listener(|this, _, window, cx| {
1694                                                let state =
1695                                                    CreateRemoteDevContainer::new(window, cx);
1696                                                this.mode = Mode::CreateRemoteDevContainer(state);
1697
1698                                                cx.notify();
1699                                            })),
1700                                    ),
1701                            ),
1702                    )
1703                    .into_any_element();
1704            }
1705            _ => {}
1706        };
1707
1708        let mut view = Navigable::new(
1709            div()
1710                .track_focus(&self.focus_handle(cx))
1711                .size_full()
1712                .child(
1713                    v_flex()
1714                        .pb_1()
1715                        .child(
1716                            ModalHeader::new()
1717                                .child(Headline::new("Dev Containers").size(HeadlineSize::XSmall)),
1718                        )
1719                        .child(ListSeparator)
1720                        .child(
1721                            div()
1722                                .id("confirm-create-from-devcontainer-json")
1723                                .track_focus(&state.entries[0].focus_handle)
1724                                .on_action(cx.listener({
1725                                    move |this, _: &menu::Confirm, window, cx| {
1726                                        this.open_dev_container(window, cx);
1727                                        this.view_in_progress_dev_container(window, cx);
1728                                    }
1729                                }))
1730                                .map(|this| {
1731                                    if state.progress == DevContainerCreationProgress::Creating {
1732                                        this.child(
1733                                            ListItem::new("creating")
1734                                                .inset(true)
1735                                                .spacing(ui::ListItemSpacing::Sparse)
1736                                                .disabled(true)
1737                                                .start_slot(
1738                                                    Icon::new(IconName::ArrowCircle)
1739                                                        .color(Color::Muted)
1740                                                        .with_rotate_animation(2),
1741                                                )
1742                                                .child(
1743                                                    h_flex()
1744                                                        .opacity(0.6)
1745                                                        .gap_1()
1746                                                        .child(Label::new("Creating From"))
1747                                                        .child(
1748                                                            Label::new("devcontainer.json")
1749                                                                .buffer_font(cx),
1750                                                        )
1751                                                        .child(LoadingLabel::new("")),
1752                                                ),
1753                                        )
1754                                    } else {
1755                                        this.child(
1756                                            ListItem::new(
1757                                                "li-confirm-create-from-devcontainer-json",
1758                                            )
1759                                            .toggle_state(
1760                                                state.entries[0]
1761                                                    .focus_handle
1762                                                    .contains_focused(window, cx),
1763                                            )
1764                                            .inset(true)
1765                                            .spacing(ui::ListItemSpacing::Sparse)
1766                                            .start_slot(
1767                                                Icon::new(IconName::Plus).color(Color::Muted),
1768                                            )
1769                                            .child(
1770                                                h_flex()
1771                                                    .gap_1()
1772                                                    .child(Label::new("Open or Create New From"))
1773                                                    .child(
1774                                                        Label::new("devcontainer.json")
1775                                                            .buffer_font(cx),
1776                                                    ),
1777                                            )
1778                                            .on_click(
1779                                                cx.listener({
1780                                                    move |this, _, window, cx| {
1781                                                        this.open_dev_container(window, cx);
1782                                                        this.view_in_progress_dev_container(
1783                                                            window, cx,
1784                                                        );
1785                                                        cx.notify();
1786                                                    }
1787                                                }),
1788                                            ),
1789                                        )
1790                                    }
1791                                }),
1792                        )
1793                        .child(
1794                            div()
1795                                .id("edit-devcontainer-json")
1796                                .track_focus(&state.entries[1].focus_handle)
1797                                .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
1798                                    this.edit_in_dev_container_json(window, cx);
1799                                }))
1800                                .child(
1801                                    ListItem::new("li-edit-devcontainer-json")
1802                                        .toggle_state(
1803                                            state.entries[1]
1804                                                .focus_handle
1805                                                .contains_focused(window, cx),
1806                                        )
1807                                        .inset(true)
1808                                        .spacing(ui::ListItemSpacing::Sparse)
1809                                        .start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
1810                                        .child(
1811                                            h_flex().gap_1().child(Label::new("Edit")).child(
1812                                                Label::new("devcontainer.json").buffer_font(cx),
1813                                            ),
1814                                        )
1815                                        .on_click(cx.listener(move |this, _, window, cx| {
1816                                            this.edit_in_dev_container_json(window, cx);
1817                                        })),
1818                                ),
1819                        )
1820                        .child(ListSeparator)
1821                        .child(
1822                            div()
1823                                .id("devcontainer-go-back")
1824                                .track_focus(&state.entries[2].focus_handle)
1825                                .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
1826                                    this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
1827                                    cx.focus_self(window);
1828                                    cx.notify();
1829                                }))
1830                                .child(
1831                                    ListItem::new("li-devcontainer-go-back")
1832                                        .toggle_state(
1833                                            state.entries[2]
1834                                                .focus_handle
1835                                                .contains_focused(window, cx),
1836                                        )
1837                                        .inset(true)
1838                                        .spacing(ui::ListItemSpacing::Sparse)
1839                                        .start_slot(
1840                                            Icon::new(IconName::ArrowLeft).color(Color::Muted),
1841                                        )
1842                                        .child(Label::new("Go Back"))
1843                                        .end_slot(
1844                                            KeyBinding::for_action_in(
1845                                                &menu::Cancel,
1846                                                &self.focus_handle,
1847                                                cx,
1848                                            )
1849                                            .size(rems_from_px(12.)),
1850                                        )
1851                                        .on_click(cx.listener(|this, _, window, cx| {
1852                                            this.mode =
1853                                                Mode::default_mode(&this.ssh_config_servers, cx);
1854                                            cx.focus_self(window);
1855                                            cx.notify()
1856                                        })),
1857                                ),
1858                        ),
1859                )
1860                .into_any_element(),
1861        );
1862
1863        view = view.entry(state.entries[0].clone());
1864        view = view.entry(state.entries[1].clone());
1865        view = view.entry(state.entries[2].clone());
1866
1867        view.render(window, cx).into_any_element()
1868    }
1869
1870    fn render_create_remote_server(
1871        &self,
1872        state: &CreateRemoteServer,
1873        window: &mut Window,
1874        cx: &mut Context<Self>,
1875    ) -> impl IntoElement {
1876        let ssh_prompt = state.ssh_prompt.clone();
1877
1878        state.address_editor.update(cx, |editor, cx| {
1879            if editor.text(cx).is_empty() {
1880                editor.set_placeholder_text("ssh user@example -p 2222", window, cx);
1881            }
1882        });
1883
1884        let theme = cx.theme();
1885
1886        v_flex()
1887            .track_focus(&self.focus_handle(cx))
1888            .id("create-remote-server")
1889            .overflow_hidden()
1890            .size_full()
1891            .flex_1()
1892            .child(
1893                div()
1894                    .p_2()
1895                    .border_b_1()
1896                    .border_color(theme.colors().border_variant)
1897                    .child(state.address_editor.clone()),
1898            )
1899            .child(
1900                h_flex()
1901                    .bg(theme.colors().editor_background)
1902                    .rounded_b_sm()
1903                    .w_full()
1904                    .map(|this| {
1905                        if let Some(ssh_prompt) = ssh_prompt {
1906                            this.child(h_flex().w_full().child(ssh_prompt))
1907                        } else if let Some(address_error) = &state.address_error {
1908                            this.child(
1909                                h_flex().p_2().w_full().gap_2().child(
1910                                    Label::new(address_error.clone())
1911                                        .size(LabelSize::Small)
1912                                        .color(Color::Error),
1913                                ),
1914                            )
1915                        } else {
1916                            this.child(
1917                                h_flex()
1918                                    .p_2()
1919                                    .w_full()
1920                                    .gap_1()
1921                                    .child(
1922                                        Label::new(
1923                                            "Enter the command you use to SSH into this server.",
1924                                        )
1925                                        .color(Color::Muted)
1926                                        .size(LabelSize::Small),
1927                                    )
1928                                    .child(
1929                                        Button::new("learn-more", "Learn More")
1930                                            .label_size(LabelSize::Small)
1931                                            .icon(IconName::ArrowUpRight)
1932                                            .icon_size(IconSize::XSmall)
1933                                            .on_click(|_, _, cx| {
1934                                                cx.open_url(
1935                                                    "https://zed.dev/docs/remote-development",
1936                                                );
1937                                            }),
1938                                    ),
1939                            )
1940                        }
1941                    }),
1942            )
1943    }
1944
1945    #[cfg(target_os = "windows")]
1946    fn render_add_wsl_distro(
1947        &self,
1948        state: &AddWslDistro,
1949        window: &mut Window,
1950        cx: &mut Context<Self>,
1951    ) -> impl IntoElement {
1952        let connection_prompt = state.connection_prompt.clone();
1953
1954        state.picker.update(cx, |picker, cx| {
1955            picker.focus_handle(cx).focus(window);
1956        });
1957
1958        v_flex()
1959            .id("add-wsl-distro")
1960            .overflow_hidden()
1961            .size_full()
1962            .flex_1()
1963            .map(|this| {
1964                if let Some(connection_prompt) = connection_prompt {
1965                    this.child(connection_prompt)
1966                } else {
1967                    this.child(state.picker.clone())
1968                }
1969            })
1970    }
1971
1972    fn render_view_options(
1973        &mut self,
1974        options: ViewServerOptionsState,
1975        window: &mut Window,
1976        cx: &mut Context<Self>,
1977    ) -> impl IntoElement {
1978        let last_entry = options.entries().last().unwrap();
1979
1980        let mut view = Navigable::new(
1981            div()
1982                .track_focus(&self.focus_handle(cx))
1983                .size_full()
1984                .child(match &options {
1985                    ViewServerOptionsState::Ssh { connection, .. } => SshConnectionHeader {
1986                        connection_string: connection.host.clone().into(),
1987                        paths: Default::default(),
1988                        nickname: connection.nickname.clone().map(|s| s.into()),
1989                        is_wsl: false,
1990                        is_devcontainer: false,
1991                    }
1992                    .render(window, cx)
1993                    .into_any_element(),
1994                    ViewServerOptionsState::Wsl { connection, .. } => SshConnectionHeader {
1995                        connection_string: connection.distro_name.clone().into(),
1996                        paths: Default::default(),
1997                        nickname: None,
1998                        is_wsl: true,
1999                        is_devcontainer: false,
2000                    }
2001                    .render(window, cx)
2002                    .into_any_element(),
2003                })
2004                .child(
2005                    v_flex()
2006                        .pb_1()
2007                        .child(ListSeparator)
2008                        .map(|this| match &options {
2009                            ViewServerOptionsState::Ssh {
2010                                connection,
2011                                entries,
2012                                server_index,
2013                            } => this.child(self.render_edit_ssh(
2014                                connection,
2015                                *server_index,
2016                                entries,
2017                                window,
2018                                cx,
2019                            )),
2020                            ViewServerOptionsState::Wsl {
2021                                connection,
2022                                entries,
2023                                server_index,
2024                            } => this.child(self.render_edit_wsl(
2025                                connection,
2026                                *server_index,
2027                                entries,
2028                                window,
2029                                cx,
2030                            )),
2031                        })
2032                        .child(ListSeparator)
2033                        .child({
2034                            div()
2035                                .id("ssh-options-copy-server-address")
2036                                .track_focus(&last_entry.focus_handle)
2037                                .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
2038                                    this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
2039                                    cx.focus_self(window);
2040                                    cx.notify();
2041                                }))
2042                                .child(
2043                                    ListItem::new("go-back")
2044                                        .toggle_state(
2045                                            last_entry.focus_handle.contains_focused(window, cx),
2046                                        )
2047                                        .inset(true)
2048                                        .spacing(ui::ListItemSpacing::Sparse)
2049                                        .start_slot(
2050                                            Icon::new(IconName::ArrowLeft).color(Color::Muted),
2051                                        )
2052                                        .child(Label::new("Go Back"))
2053                                        .on_click(cx.listener(|this, _, window, cx| {
2054                                            this.mode =
2055                                                Mode::default_mode(&this.ssh_config_servers, cx);
2056                                            cx.focus_self(window);
2057                                            cx.notify()
2058                                        })),
2059                                )
2060                        }),
2061                )
2062                .into_any_element(),
2063        );
2064
2065        for entry in options.entries() {
2066            view = view.entry(entry.clone());
2067        }
2068
2069        view.render(window, cx).into_any_element()
2070    }
2071
2072    fn render_edit_wsl(
2073        &self,
2074        connection: &WslConnectionOptions,
2075        index: WslServerIndex,
2076        entries: &[NavigableEntry],
2077        window: &mut Window,
2078        cx: &mut Context<Self>,
2079    ) -> impl IntoElement {
2080        let distro_name = SharedString::new(connection.distro_name.clone());
2081
2082        v_flex().child({
2083            fn remove_wsl_distro(
2084                remote_servers: Entity<RemoteServerProjects>,
2085                index: WslServerIndex,
2086                distro_name: SharedString,
2087                window: &mut Window,
2088                cx: &mut App,
2089            ) {
2090                let prompt_message = format!("Remove WSL distro `{}`?", distro_name);
2091
2092                let confirmation = window.prompt(
2093                    PromptLevel::Warning,
2094                    &prompt_message,
2095                    None,
2096                    &["Yes, remove it", "No, keep it"],
2097                    cx,
2098                );
2099
2100                cx.spawn(async move |cx| {
2101                    if confirmation.await.ok() == Some(0) {
2102                        remote_servers
2103                            .update(cx, |this, cx| {
2104                                this.delete_wsl_distro(index, cx);
2105                            })
2106                            .ok();
2107                        remote_servers
2108                            .update(cx, |this, cx| {
2109                                this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
2110                                cx.notify();
2111                            })
2112                            .ok();
2113                    }
2114                    anyhow::Ok(())
2115                })
2116                .detach_and_log_err(cx);
2117            }
2118            div()
2119                .id("wsl-options-remove-distro")
2120                .track_focus(&entries[0].focus_handle)
2121                .on_action(cx.listener({
2122                    let distro_name = distro_name.clone();
2123                    move |_, _: &menu::Confirm, window, cx| {
2124                        remove_wsl_distro(cx.entity(), index, distro_name.clone(), window, cx);
2125                        cx.focus_self(window);
2126                    }
2127                }))
2128                .child(
2129                    ListItem::new("remove-distro")
2130                        .toggle_state(entries[0].focus_handle.contains_focused(window, cx))
2131                        .inset(true)
2132                        .spacing(ui::ListItemSpacing::Sparse)
2133                        .start_slot(Icon::new(IconName::Trash).color(Color::Error))
2134                        .child(Label::new("Remove Distro").color(Color::Error))
2135                        .on_click(cx.listener(move |_, _, window, cx| {
2136                            remove_wsl_distro(cx.entity(), index, distro_name.clone(), window, cx);
2137                            cx.focus_self(window);
2138                        })),
2139                )
2140        })
2141    }
2142
2143    fn render_edit_ssh(
2144        &self,
2145        connection: &SshConnectionOptions,
2146        index: SshServerIndex,
2147        entries: &[NavigableEntry],
2148        window: &mut Window,
2149        cx: &mut Context<Self>,
2150    ) -> impl IntoElement {
2151        let connection_string = SharedString::new(connection.host.clone());
2152
2153        v_flex()
2154            .child({
2155                let label = if connection.nickname.is_some() {
2156                    "Edit Nickname"
2157                } else {
2158                    "Add Nickname to Server"
2159                };
2160                div()
2161                    .id("ssh-options-add-nickname")
2162                    .track_focus(&entries[0].focus_handle)
2163                    .on_action(cx.listener(move |this, _: &menu::Confirm, window, cx| {
2164                        this.mode = Mode::EditNickname(EditNicknameState::new(index, window, cx));
2165                        cx.notify();
2166                    }))
2167                    .child(
2168                        ListItem::new("add-nickname")
2169                            .toggle_state(entries[0].focus_handle.contains_focused(window, cx))
2170                            .inset(true)
2171                            .spacing(ui::ListItemSpacing::Sparse)
2172                            .start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
2173                            .child(Label::new(label))
2174                            .on_click(cx.listener(move |this, _, window, cx| {
2175                                this.mode =
2176                                    Mode::EditNickname(EditNicknameState::new(index, window, cx));
2177                                cx.notify();
2178                            })),
2179                    )
2180            })
2181            .child({
2182                let workspace = self.workspace.clone();
2183                fn callback(
2184                    workspace: WeakEntity<Workspace>,
2185                    connection_string: SharedString,
2186                    cx: &mut App,
2187                ) {
2188                    cx.write_to_clipboard(ClipboardItem::new_string(connection_string.to_string()));
2189                    workspace
2190                        .update(cx, |this, cx| {
2191                            struct SshServerAddressCopiedToClipboard;
2192                            let notification = format!(
2193                                "Copied server address ({}) to clipboard",
2194                                connection_string
2195                            );
2196
2197                            this.show_toast(
2198                                Toast::new(
2199                                    NotificationId::composite::<SshServerAddressCopiedToClipboard>(
2200                                        connection_string.clone(),
2201                                    ),
2202                                    notification,
2203                                )
2204                                .autohide(),
2205                                cx,
2206                            );
2207                        })
2208                        .ok();
2209                }
2210                div()
2211                    .id("ssh-options-copy-server-address")
2212                    .track_focus(&entries[1].focus_handle)
2213                    .on_action({
2214                        let connection_string = connection_string.clone();
2215                        let workspace = self.workspace.clone();
2216                        move |_: &menu::Confirm, _, cx| {
2217                            callback(workspace.clone(), connection_string.clone(), cx);
2218                        }
2219                    })
2220                    .child(
2221                        ListItem::new("copy-server-address")
2222                            .toggle_state(entries[1].focus_handle.contains_focused(window, cx))
2223                            .inset(true)
2224                            .spacing(ui::ListItemSpacing::Sparse)
2225                            .start_slot(Icon::new(IconName::Copy).color(Color::Muted))
2226                            .child(Label::new("Copy Server Address"))
2227                            .end_hover_slot(
2228                                Label::new(connection_string.clone()).color(Color::Muted),
2229                            )
2230                            .on_click({
2231                                let connection_string = connection_string.clone();
2232                                move |_, _, cx| {
2233                                    callback(workspace.clone(), connection_string.clone(), cx);
2234                                }
2235                            }),
2236                    )
2237            })
2238            .child({
2239                fn remove_ssh_server(
2240                    remote_servers: Entity<RemoteServerProjects>,
2241                    index: SshServerIndex,
2242                    connection_string: SharedString,
2243                    window: &mut Window,
2244                    cx: &mut App,
2245                ) {
2246                    let prompt_message = format!("Remove server `{}`?", connection_string);
2247
2248                    let confirmation = window.prompt(
2249                        PromptLevel::Warning,
2250                        &prompt_message,
2251                        None,
2252                        &["Yes, remove it", "No, keep it"],
2253                        cx,
2254                    );
2255
2256                    cx.spawn(async move |cx| {
2257                        if confirmation.await.ok() == Some(0) {
2258                            remote_servers
2259                                .update(cx, |this, cx| {
2260                                    this.delete_ssh_server(index, cx);
2261                                })
2262                                .ok();
2263                            remote_servers
2264                                .update(cx, |this, cx| {
2265                                    this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
2266                                    cx.notify();
2267                                })
2268                                .ok();
2269                        }
2270                        anyhow::Ok(())
2271                    })
2272                    .detach_and_log_err(cx);
2273                }
2274                div()
2275                    .id("ssh-options-copy-server-address")
2276                    .track_focus(&entries[2].focus_handle)
2277                    .on_action(cx.listener({
2278                        let connection_string = connection_string.clone();
2279                        move |_, _: &menu::Confirm, window, cx| {
2280                            remove_ssh_server(
2281                                cx.entity(),
2282                                index,
2283                                connection_string.clone(),
2284                                window,
2285                                cx,
2286                            );
2287                            cx.focus_self(window);
2288                        }
2289                    }))
2290                    .child(
2291                        ListItem::new("remove-server")
2292                            .toggle_state(entries[2].focus_handle.contains_focused(window, cx))
2293                            .inset(true)
2294                            .spacing(ui::ListItemSpacing::Sparse)
2295                            .start_slot(Icon::new(IconName::Trash).color(Color::Error))
2296                            .child(Label::new("Remove Server").color(Color::Error))
2297                            .on_click(cx.listener(move |_, _, window, cx| {
2298                                remove_ssh_server(
2299                                    cx.entity(),
2300                                    index,
2301                                    connection_string.clone(),
2302                                    window,
2303                                    cx,
2304                                );
2305                                cx.focus_self(window);
2306                            })),
2307                    )
2308            })
2309    }
2310
2311    fn render_edit_nickname(
2312        &self,
2313        state: &EditNicknameState,
2314        window: &mut Window,
2315        cx: &mut Context<Self>,
2316    ) -> impl IntoElement {
2317        let Some(connection) = SshSettings::get_global(cx)
2318            .ssh_connections()
2319            .nth(state.index.0)
2320        else {
2321            return v_flex()
2322                .id("ssh-edit-nickname")
2323                .track_focus(&self.focus_handle(cx));
2324        };
2325
2326        let connection_string = connection.host.clone();
2327        let nickname = connection.nickname.map(|s| s.into());
2328
2329        v_flex()
2330            .id("ssh-edit-nickname")
2331            .track_focus(&self.focus_handle(cx))
2332            .child(
2333                SshConnectionHeader {
2334                    connection_string,
2335                    paths: Default::default(),
2336                    nickname,
2337                    is_wsl: false,
2338                    is_devcontainer: false,
2339                }
2340                .render(window, cx),
2341            )
2342            .child(
2343                h_flex()
2344                    .p_2()
2345                    .border_t_1()
2346                    .border_color(cx.theme().colors().border_variant)
2347                    .child(state.editor.clone()),
2348            )
2349    }
2350
2351    fn render_default(
2352        &mut self,
2353        mut state: DefaultState,
2354        window: &mut Window,
2355        cx: &mut Context<Self>,
2356    ) -> impl IntoElement {
2357        let ssh_settings = SshSettings::get_global(cx);
2358        let mut should_rebuild = false;
2359
2360        let ssh_connections_changed = ssh_settings.ssh_connections.0.iter().ne(state
2361            .servers
2362            .iter()
2363            .filter_map(|server| match server {
2364                RemoteEntry::Project {
2365                    connection: Connection::Ssh(connection),
2366                    ..
2367                } => Some(connection),
2368                _ => None,
2369            }));
2370
2371        let wsl_connections_changed = ssh_settings.wsl_connections.0.iter().ne(state
2372            .servers
2373            .iter()
2374            .filter_map(|server| match server {
2375                RemoteEntry::Project {
2376                    connection: Connection::Wsl(connection),
2377                    ..
2378                } => Some(connection),
2379                _ => None,
2380            }));
2381
2382        if ssh_connections_changed || wsl_connections_changed {
2383            should_rebuild = true;
2384        };
2385
2386        if !should_rebuild && ssh_settings.read_ssh_config {
2387            let current_ssh_hosts: BTreeSet<SharedString> = state
2388                .servers
2389                .iter()
2390                .filter_map(|server| match server {
2391                    RemoteEntry::SshConfig { host, .. } => Some(host.clone()),
2392                    _ => None,
2393                })
2394                .collect();
2395            let mut expected_ssh_hosts = self.ssh_config_servers.clone();
2396            for server in &state.servers {
2397                if let RemoteEntry::Project {
2398                    connection: Connection::Ssh(connection),
2399                    ..
2400                } = server
2401                {
2402                    expected_ssh_hosts.remove(&connection.host);
2403                }
2404            }
2405            should_rebuild = current_ssh_hosts != expected_ssh_hosts;
2406        }
2407
2408        if should_rebuild {
2409            self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
2410            if let Mode::Default(new_state) = &self.mode {
2411                state = new_state.clone();
2412            }
2413        }
2414
2415        let connect_button = div()
2416            .id("ssh-connect-new-server-container")
2417            .track_focus(&state.add_new_server.focus_handle)
2418            .anchor_scroll(state.add_new_server.scroll_anchor.clone())
2419            .child(
2420                ListItem::new("register-remote-server-button")
2421                    .toggle_state(
2422                        state
2423                            .add_new_server
2424                            .focus_handle
2425                            .contains_focused(window, cx),
2426                    )
2427                    .inset(true)
2428                    .spacing(ui::ListItemSpacing::Sparse)
2429                    .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
2430                    .child(Label::new("Connect SSH Server"))
2431                    .on_click(cx.listener(|this, _, window, cx| {
2432                        let state = CreateRemoteServer::new(window, cx);
2433                        this.mode = Mode::CreateRemoteServer(state);
2434
2435                        cx.notify();
2436                    })),
2437            )
2438            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
2439                let state = CreateRemoteServer::new(window, cx);
2440                this.mode = Mode::CreateRemoteServer(state);
2441
2442                cx.notify();
2443            }));
2444
2445        let connect_dev_container_button = div()
2446            .id("connect-new-dev-container")
2447            .track_focus(&state.add_new_devcontainer.focus_handle)
2448            .anchor_scroll(state.add_new_devcontainer.scroll_anchor.clone())
2449            .child(
2450                ListItem::new("register-dev-container-button")
2451                    .toggle_state(
2452                        state
2453                            .add_new_devcontainer
2454                            .focus_handle
2455                            .contains_focused(window, cx),
2456                    )
2457                    .inset(true)
2458                    .spacing(ui::ListItemSpacing::Sparse)
2459                    .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
2460                    .child(Label::new("Connect Dev Container"))
2461                    .on_click(cx.listener(|this, _, window, cx| {
2462                        let state = CreateRemoteDevContainer::new(window, cx);
2463                        this.mode = Mode::CreateRemoteDevContainer(state);
2464
2465                        cx.notify();
2466                    })),
2467            )
2468            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
2469                let state = CreateRemoteDevContainer::new(window, cx);
2470                this.mode = Mode::CreateRemoteDevContainer(state);
2471
2472                cx.notify();
2473            }));
2474
2475        #[cfg(target_os = "windows")]
2476        let wsl_connect_button = div()
2477            .id("wsl-connect-new-server")
2478            .track_focus(&state.add_new_wsl.focus_handle)
2479            .anchor_scroll(state.add_new_wsl.scroll_anchor.clone())
2480            .child(
2481                ListItem::new("wsl-add-new-server")
2482                    .toggle_state(state.add_new_wsl.focus_handle.contains_focused(window, cx))
2483                    .inset(true)
2484                    .spacing(ui::ListItemSpacing::Sparse)
2485                    .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
2486                    .child(Label::new("Add WSL Distro"))
2487                    .on_click(cx.listener(|this, _, window, cx| {
2488                        let state = AddWslDistro::new(window, cx);
2489                        this.mode = Mode::AddWslDistro(state);
2490
2491                        cx.notify();
2492                    })),
2493            )
2494            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
2495                let state = AddWslDistro::new(window, cx);
2496                this.mode = Mode::AddWslDistro(state);
2497
2498                cx.notify();
2499            }));
2500
2501        let has_open_project = self
2502            .workspace
2503            .upgrade()
2504            .map(|workspace| {
2505                workspace
2506                    .read(cx)
2507                    .project()
2508                    .read(cx)
2509                    .visible_worktrees(cx)
2510                    .next()
2511                    .is_some()
2512            })
2513            .unwrap_or(false);
2514
2515        let modal_section = v_flex()
2516            .track_focus(&self.focus_handle(cx))
2517            .id("ssh-server-list")
2518            .overflow_y_scroll()
2519            .track_scroll(&state.scroll_handle)
2520            .size_full()
2521            .child(connect_button)
2522            .when(has_open_project, |this| {
2523                this.child(connect_dev_container_button)
2524            });
2525
2526        #[cfg(target_os = "windows")]
2527        let modal_section = modal_section.child(wsl_connect_button);
2528        #[cfg(not(target_os = "windows"))]
2529        let modal_section = modal_section;
2530
2531        let mut modal_section = Navigable::new(
2532            modal_section
2533                .child(
2534                    List::new()
2535                        .empty_message(
2536                            h_flex()
2537                                .size_full()
2538                                .p_2()
2539                                .justify_center()
2540                                .border_t_1()
2541                                .border_color(cx.theme().colors().border_variant)
2542                                .child(
2543                                    Label::new("No remote servers registered yet.")
2544                                        .color(Color::Muted),
2545                                )
2546                                .into_any_element(),
2547                        )
2548                        .children(state.servers.iter().enumerate().map(|(ix, connection)| {
2549                            self.render_remote_connection(ix, connection.clone(), window, cx)
2550                                .into_any_element()
2551                        })),
2552                )
2553                .into_any_element(),
2554        )
2555        .entry(state.add_new_server.clone());
2556
2557        if has_open_project {
2558            modal_section = modal_section.entry(state.add_new_devcontainer.clone());
2559        }
2560
2561        if cfg!(target_os = "windows") {
2562            modal_section = modal_section.entry(state.add_new_wsl.clone());
2563        }
2564
2565        for server in &state.servers {
2566            match server {
2567                RemoteEntry::Project {
2568                    open_folder,
2569                    projects,
2570                    configure,
2571                    ..
2572                } => {
2573                    for (navigation_state, _) in projects {
2574                        modal_section = modal_section.entry(navigation_state.clone());
2575                    }
2576                    modal_section = modal_section
2577                        .entry(open_folder.clone())
2578                        .entry(configure.clone());
2579                }
2580                RemoteEntry::SshConfig { open_folder, .. } => {
2581                    modal_section = modal_section.entry(open_folder.clone());
2582                }
2583            }
2584        }
2585        let mut modal_section = modal_section.render(window, cx).into_any_element();
2586
2587        let (create_window, reuse_window) = if self.create_new_window {
2588            (
2589                window.keystroke_text_for(&menu::Confirm),
2590                window.keystroke_text_for(&menu::SecondaryConfirm),
2591            )
2592        } else {
2593            (
2594                window.keystroke_text_for(&menu::SecondaryConfirm),
2595                window.keystroke_text_for(&menu::Confirm),
2596            )
2597        };
2598        let placeholder_text = Arc::from(format!(
2599            "{reuse_window} reuses this window, {create_window} opens a new one",
2600        ));
2601
2602        Modal::new("remote-projects", None)
2603            .header(
2604                ModalHeader::new()
2605                    .child(Headline::new("Remote Projects").size(HeadlineSize::XSmall))
2606                    .child(
2607                        Label::new(placeholder_text)
2608                            .color(Color::Muted)
2609                            .size(LabelSize::XSmall),
2610                    ),
2611            )
2612            .section(
2613                Section::new().padded(false).child(
2614                    v_flex()
2615                        .min_h(rems(20.))
2616                        .size_full()
2617                        .relative()
2618                        .child(ListSeparator)
2619                        .child(
2620                            canvas(
2621                                |bounds, window, cx| {
2622                                    modal_section.prepaint_as_root(
2623                                        bounds.origin,
2624                                        bounds.size.into(),
2625                                        window,
2626                                        cx,
2627                                    );
2628                                    modal_section
2629                                },
2630                                |_, mut modal_section, window, cx| {
2631                                    modal_section.paint(window, cx);
2632                                },
2633                            )
2634                            .size_full(),
2635                        )
2636                        .vertical_scrollbar_for(&state.scroll_handle, window, cx),
2637                ),
2638            )
2639            .into_any_element()
2640    }
2641
2642    fn create_host_from_ssh_config(
2643        &mut self,
2644        ssh_config_host: &SharedString,
2645        cx: &mut Context<'_, Self>,
2646    ) -> SshServerIndex {
2647        let new_ix = Arc::new(AtomicUsize::new(0));
2648
2649        let update_new_ix = new_ix.clone();
2650        self.update_settings_file(cx, move |settings, _| {
2651            update_new_ix.store(
2652                settings
2653                    .ssh_connections
2654                    .as_ref()
2655                    .map_or(0, |connections| connections.len()),
2656                atomic::Ordering::Release,
2657            );
2658        });
2659
2660        self.add_ssh_server(
2661            SshConnectionOptions {
2662                host: ssh_config_host.to_string(),
2663                ..SshConnectionOptions::default()
2664            },
2665            cx,
2666        );
2667        self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
2668        SshServerIndex(new_ix.load(atomic::Ordering::Acquire))
2669    }
2670}
2671
2672fn spawn_ssh_config_watch(fs: Arc<dyn Fs>, cx: &Context<RemoteServerProjects>) -> Task<()> {
2673    let mut user_ssh_config_watcher =
2674        watch_config_file(cx.background_executor(), fs.clone(), user_ssh_config_file());
2675    let mut global_ssh_config_watcher = global_ssh_config_file()
2676        .map(|it| watch_config_file(cx.background_executor(), fs, it.to_owned()))
2677        .unwrap_or_else(|| futures::channel::mpsc::unbounded().1);
2678
2679    cx.spawn(async move |remote_server_projects, cx| {
2680        let mut global_hosts = BTreeSet::default();
2681        let mut user_hosts = BTreeSet::default();
2682        let mut running_receivers = 2;
2683
2684        loop {
2685            select! {
2686                new_global_file_contents = global_ssh_config_watcher.next().fuse() => {
2687                    match new_global_file_contents {
2688                        Some(new_global_file_contents) => {
2689                            global_hosts = parse_ssh_config_hosts(&new_global_file_contents);
2690                            if remote_server_projects.update(cx, |remote_server_projects, cx| {
2691                                remote_server_projects.ssh_config_servers = global_hosts.iter().chain(user_hosts.iter()).map(SharedString::from).collect();
2692                                cx.notify();
2693                            }).is_err() {
2694                                return;
2695                            }
2696                        },
2697                        None => {
2698                            running_receivers -= 1;
2699                            if running_receivers == 0 {
2700                                return;
2701                            }
2702                        }
2703                    }
2704                },
2705                new_user_file_contents = user_ssh_config_watcher.next().fuse() => {
2706                    match new_user_file_contents {
2707                        Some(new_user_file_contents) => {
2708                            user_hosts = parse_ssh_config_hosts(&new_user_file_contents);
2709                            if remote_server_projects.update(cx, |remote_server_projects, cx| {
2710                                remote_server_projects.ssh_config_servers = global_hosts.iter().chain(user_hosts.iter()).map(SharedString::from).collect();
2711                                cx.notify();
2712                            }).is_err() {
2713                                return;
2714                            }
2715                        },
2716                        None => {
2717                            running_receivers -= 1;
2718                            if running_receivers == 0 {
2719                                return;
2720                            }
2721                        }
2722                    }
2723                },
2724            }
2725        }
2726    })
2727}
2728
2729fn get_text(element: &Entity<Editor>, cx: &mut App) -> String {
2730    element.read(cx).text(cx).trim().to_string()
2731}
2732
2733impl ModalView for RemoteServerProjects {}
2734
2735impl Focusable for RemoteServerProjects {
2736    fn focus_handle(&self, cx: &App) -> FocusHandle {
2737        match &self.mode {
2738            Mode::ProjectPicker(picker) => picker.focus_handle(cx),
2739            _ => self.focus_handle.clone(),
2740        }
2741    }
2742}
2743
2744impl EventEmitter<DismissEvent> for RemoteServerProjects {}
2745
2746impl Render for RemoteServerProjects {
2747    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2748        div()
2749            .elevation_3(cx)
2750            .w(rems(34.))
2751            .key_context("RemoteServerModal")
2752            .on_action(cx.listener(Self::cancel))
2753            .on_action(cx.listener(Self::confirm))
2754            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
2755                this.focus_handle(cx).focus(window);
2756            }))
2757            .on_mouse_down_out(cx.listener(|this, _, _, cx| {
2758                if matches!(this.mode, Mode::Default(_)) {
2759                    cx.emit(DismissEvent)
2760                }
2761            }))
2762            .child(match &self.mode {
2763                Mode::Default(state) => self
2764                    .render_default(state.clone(), window, cx)
2765                    .into_any_element(),
2766                Mode::ViewServerOptions(state) => self
2767                    .render_view_options(state.clone(), window, cx)
2768                    .into_any_element(),
2769                Mode::ProjectPicker(element) => element.clone().into_any_element(),
2770                Mode::CreateRemoteServer(state) => self
2771                    .render_create_remote_server(state, window, cx)
2772                    .into_any_element(),
2773                Mode::CreateRemoteDevContainer(state) => self
2774                    .render_create_dev_container(state, window, cx)
2775                    .into_any_element(),
2776                Mode::EditNickname(state) => self
2777                    .render_edit_nickname(state, window, cx)
2778                    .into_any_element(),
2779                #[cfg(target_os = "windows")]
2780                Mode::AddWslDistro(state) => self
2781                    .render_add_wsl_distro(state, window, cx)
2782                    .into_any_element(),
2783            })
2784    }
2785}