remote_servers.rs

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