remote_servers.rs

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