remote_servers.rs

   1use std::any::Any;
   2use std::borrow::Cow;
   3use std::collections::BTreeSet;
   4use std::path::PathBuf;
   5use std::rc::Rc;
   6use std::sync::Arc;
   7use std::sync::atomic;
   8use std::sync::atomic::AtomicUsize;
   9
  10use editor::Editor;
  11use file_finder::OpenPathDelegate;
  12use futures::FutureExt;
  13use futures::channel::oneshot;
  14use futures::future::Shared;
  15use futures::select;
  16use gpui::ClipboardItem;
  17use gpui::Subscription;
  18use gpui::Task;
  19use gpui::WeakEntity;
  20use gpui::canvas;
  21use gpui::{
  22    AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
  23    PromptLevel, ScrollHandle, Window,
  24};
  25use paths::global_ssh_config_file;
  26use paths::user_ssh_config_file;
  27use picker::Picker;
  28use project::Fs;
  29use project::Project;
  30use remote::SshConnectionOptions;
  31use remote::SshRemoteClient;
  32use remote::ssh_session::ConnectionIdentifier;
  33use settings::Settings;
  34use settings::SettingsStore;
  35use settings::update_settings_file;
  36use settings::watch_config_file;
  37use smol::stream::StreamExt as _;
  38use ui::Navigable;
  39use ui::NavigableEntry;
  40use ui::{
  41    IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState,
  42    Section, Tooltip, prelude::*,
  43};
  44use util::ResultExt;
  45use workspace::OpenOptions;
  46use workspace::Toast;
  47use workspace::notifications::NotificationId;
  48use workspace::{
  49    ModalView, Workspace, notifications::DetachAndPromptErr,
  50    open_ssh_project_with_existing_connection,
  51};
  52
  53use crate::OpenRemote;
  54use crate::ssh_config::parse_ssh_config_hosts;
  55use crate::ssh_connections::RemoteSettingsContent;
  56use crate::ssh_connections::SshConnection;
  57use crate::ssh_connections::SshConnectionHeader;
  58use crate::ssh_connections::SshConnectionModal;
  59use crate::ssh_connections::SshProject;
  60use crate::ssh_connections::SshPrompt;
  61use crate::ssh_connections::SshSettings;
  62use crate::ssh_connections::connect_over_ssh;
  63use crate::ssh_connections::open_ssh_project;
  64
  65mod navigation_base {}
  66pub struct RemoteServerProjects {
  67    mode: Mode,
  68    focus_handle: FocusHandle,
  69    workspace: WeakEntity<Workspace>,
  70    retained_connections: Vec<Entity<SshRemoteClient>>,
  71    ssh_config_updates: Task<()>,
  72    ssh_config_servers: BTreeSet<SharedString>,
  73    _subscription: Subscription,
  74}
  75
  76struct CreateRemoteServer {
  77    address_editor: Entity<Editor>,
  78    address_error: Option<SharedString>,
  79    ssh_prompt: Option<Entity<SshPrompt>>,
  80    _creating: Option<Task<Option<()>>>,
  81}
  82
  83impl CreateRemoteServer {
  84    fn new(window: &mut Window, cx: &mut App) -> Self {
  85        let address_editor = cx.new(|cx| Editor::single_line(window, cx));
  86        address_editor.update(cx, |this, cx| {
  87            this.focus_handle(cx).focus(window);
  88        });
  89        Self {
  90            address_editor,
  91            address_error: None,
  92            ssh_prompt: None,
  93            _creating: None,
  94        }
  95    }
  96}
  97
  98struct ProjectPicker {
  99    connection_string: SharedString,
 100    nickname: Option<SharedString>,
 101    picker: Entity<Picker<OpenPathDelegate>>,
 102    _path_task: Shared<Task<Option<()>>>,
 103}
 104
 105struct EditNicknameState {
 106    index: usize,
 107    editor: Entity<Editor>,
 108}
 109
 110impl EditNicknameState {
 111    fn new(index: usize, window: &mut Window, cx: &mut App) -> Self {
 112        let this = Self {
 113            index,
 114            editor: cx.new(|cx| Editor::single_line(window, cx)),
 115        };
 116        let starting_text = SshSettings::get_global(cx)
 117            .ssh_connections()
 118            .nth(index)
 119            .and_then(|state| state.nickname.clone())
 120            .filter(|text| !text.is_empty());
 121        this.editor.update(cx, |this, cx| {
 122            this.set_placeholder_text("Add a nickname for this server", cx);
 123            if let Some(starting_text) = starting_text {
 124                this.set_text(starting_text, window, cx);
 125            }
 126        });
 127        this.editor.focus_handle(cx).focus(window);
 128        this
 129    }
 130}
 131
 132impl Focusable for ProjectPicker {
 133    fn focus_handle(&self, cx: &App) -> FocusHandle {
 134        self.picker.focus_handle(cx)
 135    }
 136}
 137
 138impl ProjectPicker {
 139    fn new(
 140        ix: usize,
 141        connection: SshConnectionOptions,
 142        project: Entity<Project>,
 143        home_dir: PathBuf,
 144        workspace: WeakEntity<Workspace>,
 145        window: &mut Window,
 146        cx: &mut Context<RemoteServerProjects>,
 147    ) -> Entity<Self> {
 148        let (tx, rx) = oneshot::channel();
 149        let lister = project::DirectoryLister::Project(project.clone());
 150        let delegate = file_finder::OpenPathDelegate::new(tx, lister);
 151
 152        let picker = cx.new(|cx| {
 153            let picker = Picker::uniform_list(delegate, window, cx)
 154                .width(rems(34.))
 155                .modal(false);
 156            picker.set_query(home_dir.to_string_lossy().to_string(), window, cx);
 157            picker
 158        });
 159        let connection_string = connection.connection_string().into();
 160        let nickname = connection.nickname.clone().map(|nick| nick.into());
 161        let _path_task = cx
 162            .spawn_in(window, {
 163                let workspace = workspace.clone();
 164                async move |this, cx| {
 165                    let Ok(Some(paths)) = rx.await else {
 166                        workspace
 167                            .update_in(cx, |workspace, window, cx| {
 168                                let fs = workspace.project().read(cx).fs().clone();
 169                                let weak = cx.entity().downgrade();
 170                                workspace.toggle_modal(window, cx, |window, cx| {
 171                                    RemoteServerProjects::new(fs, window, cx, weak)
 172                                });
 173                            })
 174                            .log_err()?;
 175                        return None;
 176                    };
 177
 178                    let app_state = workspace
 179                        .update(cx, |workspace, _| workspace.app_state().clone())
 180                        .ok()?;
 181
 182                    cx.update(|_, cx| {
 183                        let fs = app_state.fs.clone();
 184                        update_settings_file::<SshSettings>(fs, cx, {
 185                            let paths = paths
 186                                .iter()
 187                                .map(|path| path.to_string_lossy().to_string())
 188                                .collect();
 189                            move |setting, _| {
 190                                if let Some(server) = setting
 191                                    .ssh_connections
 192                                    .as_mut()
 193                                    .and_then(|connections| connections.get_mut(ix))
 194                                {
 195                                    server.projects.insert(SshProject { paths });
 196                                }
 197                            }
 198                        });
 199                    })
 200                    .log_err();
 201
 202                    let options = cx
 203                        .update(|_, cx| (app_state.build_window_options)(None, cx))
 204                        .log_err()?;
 205                    let window = cx
 206                        .open_window(options, |window, cx| {
 207                            cx.new(|cx| {
 208                                telemetry::event!("SSH Project Created");
 209                                Workspace::new(None, project.clone(), app_state.clone(), window, cx)
 210                            })
 211                        })
 212                        .log_err()?;
 213
 214                    open_ssh_project_with_existing_connection(
 215                        connection, project, paths, app_state, window, cx,
 216                    )
 217                    .await
 218                    .log_err();
 219
 220                    this.update(cx, |_, cx| {
 221                        cx.emit(DismissEvent);
 222                    })
 223                    .ok();
 224                    Some(())
 225                }
 226            })
 227            .shared();
 228        cx.new(|_| Self {
 229            _path_task,
 230            picker,
 231            connection_string,
 232            nickname,
 233        })
 234    }
 235}
 236
 237impl gpui::Render for ProjectPicker {
 238    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 239        v_flex()
 240            .child(
 241                SshConnectionHeader {
 242                    connection_string: self.connection_string.clone(),
 243                    paths: Default::default(),
 244                    nickname: self.nickname.clone(),
 245                }
 246                .render(window, cx),
 247            )
 248            .child(
 249                div()
 250                    .border_t_1()
 251                    .border_color(cx.theme().colors().border_variant)
 252                    .child(self.picker.clone()),
 253            )
 254    }
 255}
 256
 257#[derive(Clone)]
 258enum RemoteEntry {
 259    Project {
 260        open_folder: NavigableEntry,
 261        projects: Vec<(NavigableEntry, SshProject)>,
 262        configure: NavigableEntry,
 263        connection: SshConnection,
 264    },
 265    SshConfig {
 266        open_folder: NavigableEntry,
 267        host: SharedString,
 268    },
 269}
 270
 271impl RemoteEntry {
 272    fn is_from_zed(&self) -> bool {
 273        matches!(self, Self::Project { .. })
 274    }
 275
 276    fn connection(&self) -> Cow<SshConnection> {
 277        match self {
 278            Self::Project { connection, .. } => Cow::Borrowed(connection),
 279            Self::SshConfig { host, .. } => Cow::Owned(SshConnection {
 280                host: host.clone(),
 281                ..SshConnection::default()
 282            }),
 283        }
 284    }
 285}
 286
 287#[derive(Clone)]
 288struct DefaultState {
 289    scrollbar: ScrollbarState,
 290    add_new_server: NavigableEntry,
 291    servers: Vec<RemoteEntry>,
 292    handle: ScrollHandle,
 293}
 294
 295impl DefaultState {
 296    fn new(cx: &mut App) -> Self {
 297        let handle = ScrollHandle::new();
 298        let scrollbar = ScrollbarState::new(handle.clone());
 299        let add_new_server = NavigableEntry::new(&handle, cx);
 300        let servers = SshSettings::get_global(cx)
 301            .ssh_connections()
 302            .map(|connection| {
 303                let open_folder = NavigableEntry::new(&handle, cx);
 304                let configure = NavigableEntry::new(&handle, cx);
 305                let projects = connection
 306                    .projects
 307                    .iter()
 308                    .map(|project| (NavigableEntry::new(&handle, cx), project.clone()))
 309                    .collect();
 310                RemoteEntry::Project {
 311                    open_folder,
 312                    configure,
 313                    projects,
 314                    connection,
 315                }
 316            })
 317            .collect();
 318
 319        Self {
 320            scrollbar,
 321            add_new_server,
 322            servers,
 323            handle,
 324        }
 325    }
 326}
 327
 328#[derive(Clone)]
 329struct ViewServerOptionsState {
 330    server_index: usize,
 331    connection: SshConnection,
 332    entries: [NavigableEntry; 4],
 333}
 334enum Mode {
 335    Default(DefaultState),
 336    ViewServerOptions(ViewServerOptionsState),
 337    EditNickname(EditNicknameState),
 338    ProjectPicker(Entity<ProjectPicker>),
 339    CreateRemoteServer(CreateRemoteServer),
 340}
 341
 342impl Mode {
 343    fn default_mode(cx: &mut App) -> Self {
 344        Self::Default(DefaultState::new(cx))
 345    }
 346}
 347impl RemoteServerProjects {
 348    pub fn register(
 349        workspace: &mut Workspace,
 350        _window: Option<&mut Window>,
 351        _: &mut Context<Workspace>,
 352    ) {
 353        workspace.register_action(|workspace, _: &OpenRemote, window, cx| {
 354            let handle = cx.entity().downgrade();
 355            let fs = workspace.project().read(cx).fs().clone();
 356            workspace.toggle_modal(window, cx, |window, cx| Self::new(fs, window, cx, handle))
 357        });
 358    }
 359
 360    pub fn open(workspace: Entity<Workspace>, window: &mut Window, cx: &mut App) {
 361        workspace.update(cx, |workspace, cx| {
 362            let handle = cx.entity().downgrade();
 363            let fs = workspace.project().read(cx).fs().clone();
 364            workspace.toggle_modal(window, cx, |window, cx| Self::new(fs, window, cx, handle))
 365        })
 366    }
 367
 368    pub fn new(
 369        fs: Arc<dyn Fs>,
 370        window: &mut Window,
 371        cx: &mut Context<Self>,
 372        workspace: WeakEntity<Workspace>,
 373    ) -> Self {
 374        let focus_handle = cx.focus_handle();
 375        let mut read_ssh_config = SshSettings::get_global(cx).read_ssh_config;
 376        let ssh_config_updates = if read_ssh_config {
 377            spawn_ssh_config_watch(fs.clone(), cx)
 378        } else {
 379            Task::ready(())
 380        };
 381
 382        let mut base_style = window.text_style();
 383        base_style.refine(&gpui::TextStyleRefinement {
 384            color: Some(cx.theme().colors().editor_foreground),
 385            ..Default::default()
 386        });
 387
 388        let _subscription =
 389            cx.observe_global_in::<SettingsStore>(window, move |recent_projects, _, cx| {
 390                let new_read_ssh_config = SshSettings::get_global(cx).read_ssh_config;
 391                if read_ssh_config != new_read_ssh_config {
 392                    read_ssh_config = new_read_ssh_config;
 393                    if read_ssh_config {
 394                        recent_projects.ssh_config_updates = spawn_ssh_config_watch(fs.clone(), cx);
 395                    } else {
 396                        recent_projects.ssh_config_servers.clear();
 397                        recent_projects.ssh_config_updates = Task::ready(());
 398                    }
 399                }
 400            });
 401
 402        Self {
 403            mode: Mode::default_mode(cx),
 404            focus_handle,
 405            workspace,
 406            retained_connections: Vec::new(),
 407            ssh_config_updates,
 408            ssh_config_servers: BTreeSet::new(),
 409            _subscription,
 410        }
 411    }
 412
 413    pub fn project_picker(
 414        ix: usize,
 415        connection_options: remote::SshConnectionOptions,
 416        project: Entity<Project>,
 417        home_dir: PathBuf,
 418        window: &mut Window,
 419        cx: &mut Context<Self>,
 420        workspace: WeakEntity<Workspace>,
 421    ) -> Self {
 422        let fs = project.read(cx).fs().clone();
 423        let mut this = Self::new(fs, window, cx, workspace.clone());
 424        this.mode = Mode::ProjectPicker(ProjectPicker::new(
 425            ix,
 426            connection_options,
 427            project,
 428            home_dir,
 429            workspace,
 430            window,
 431            cx,
 432        ));
 433        cx.notify();
 434
 435        this
 436    }
 437
 438    fn create_ssh_server(
 439        &mut self,
 440        editor: Entity<Editor>,
 441        window: &mut Window,
 442        cx: &mut Context<Self>,
 443    ) {
 444        let input = get_text(&editor, cx);
 445        if input.is_empty() {
 446            return;
 447        }
 448
 449        let connection_options = match SshConnectionOptions::parse_command_line(&input) {
 450            Ok(c) => c,
 451            Err(e) => {
 452                self.mode = Mode::CreateRemoteServer(CreateRemoteServer {
 453                    address_editor: editor,
 454                    address_error: Some(format!("could not parse: {:?}", e).into()),
 455                    ssh_prompt: None,
 456                    _creating: None,
 457                });
 458                return;
 459            }
 460        };
 461        let ssh_prompt = cx.new(|cx| SshPrompt::new(&connection_options, window, cx));
 462
 463        let connection = connect_over_ssh(
 464            ConnectionIdentifier::setup(),
 465            connection_options.clone(),
 466            ssh_prompt.clone(),
 467            window,
 468            cx,
 469        )
 470        .prompt_err("Failed to connect", window, cx, |_, _, _| None);
 471
 472        let address_editor = editor.clone();
 473        let creating = cx.spawn(async move |this, cx| {
 474            match connection.await {
 475                Some(Some(client)) => this
 476                    .update(cx, |this, cx| {
 477                        telemetry::event!("SSH Server Created");
 478                        this.retained_connections.push(client);
 479                        this.add_ssh_server(connection_options, cx);
 480                        this.mode = Mode::default_mode(cx);
 481                        cx.notify()
 482                    })
 483                    .log_err(),
 484                _ => this
 485                    .update(cx, |this, cx| {
 486                        address_editor.update(cx, |this, _| {
 487                            this.set_read_only(false);
 488                        });
 489                        this.mode = Mode::CreateRemoteServer(CreateRemoteServer {
 490                            address_editor,
 491                            address_error: None,
 492                            ssh_prompt: None,
 493                            _creating: None,
 494                        });
 495                        cx.notify()
 496                    })
 497                    .log_err(),
 498            };
 499            None
 500        });
 501
 502        editor.update(cx, |this, _| {
 503            this.set_read_only(true);
 504        });
 505        self.mode = Mode::CreateRemoteServer(CreateRemoteServer {
 506            address_editor: editor,
 507            address_error: None,
 508            ssh_prompt: Some(ssh_prompt.clone()),
 509            _creating: Some(creating),
 510        });
 511    }
 512
 513    fn view_server_options(
 514        &mut self,
 515        (server_index, connection): (usize, SshConnection),
 516        window: &mut Window,
 517        cx: &mut Context<Self>,
 518    ) {
 519        self.mode = Mode::ViewServerOptions(ViewServerOptionsState {
 520            server_index,
 521            connection,
 522            entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)),
 523        });
 524        self.focus_handle(cx).focus(window);
 525        cx.notify();
 526    }
 527
 528    fn create_ssh_project(
 529        &mut self,
 530        ix: usize,
 531        ssh_connection: SshConnection,
 532        window: &mut Window,
 533        cx: &mut Context<Self>,
 534    ) {
 535        let Some(workspace) = self.workspace.upgrade() else {
 536            return;
 537        };
 538
 539        let connection_options = ssh_connection.into();
 540        workspace.update(cx, |_, cx| {
 541            cx.defer_in(window, move |workspace, window, cx| {
 542                let app_state = workspace.app_state().clone();
 543                workspace.toggle_modal(window, cx, |window, cx| {
 544                    SshConnectionModal::new(&connection_options, Vec::new(), window, cx)
 545                });
 546                let prompt = workspace
 547                    .active_modal::<SshConnectionModal>(cx)
 548                    .unwrap()
 549                    .read(cx)
 550                    .prompt
 551                    .clone();
 552
 553                let connect = connect_over_ssh(
 554                    ConnectionIdentifier::setup(),
 555                    connection_options.clone(),
 556                    prompt,
 557                    window,
 558                    cx,
 559                )
 560                .prompt_err("Failed to connect", window, cx, |_, _, _| None);
 561
 562                cx.spawn_in(window, async move |workspace, cx| {
 563                    let session = connect.await;
 564
 565                    workspace.update(cx, |workspace, cx| {
 566                        if let Some(prompt) = workspace.active_modal::<SshConnectionModal>(cx) {
 567                            prompt.update(cx, |prompt, cx| prompt.finished(cx))
 568                        }
 569                    })?;
 570
 571                    let Some(Some(session)) = session else {
 572                        return workspace.update_in(cx, |workspace, window, cx| {
 573                            let weak = cx.entity().downgrade();
 574                            let fs = workspace.project().read(cx).fs().clone();
 575                            workspace.toggle_modal(window, cx, |window, cx| {
 576                                RemoteServerProjects::new(fs, window, cx, weak)
 577                            });
 578                        });
 579                    };
 580
 581                    let project = cx.update(|_, cx| {
 582                        project::Project::ssh(
 583                            session,
 584                            app_state.client.clone(),
 585                            app_state.node_runtime.clone(),
 586                            app_state.user_store.clone(),
 587                            app_state.languages.clone(),
 588                            app_state.fs.clone(),
 589                            cx,
 590                        )
 591                    })?;
 592
 593                    let home_dir = project
 594                        .read_with(cx, |project, cx| project.resolve_abs_path("~", cx))?
 595                        .await
 596                        .and_then(|path| path.into_abs_path())
 597                        .unwrap_or(PathBuf::from("/"));
 598
 599                    workspace
 600                        .update_in(cx, |workspace, window, cx| {
 601                            let weak = cx.entity().downgrade();
 602                            workspace.toggle_modal(window, cx, |window, cx| {
 603                                RemoteServerProjects::project_picker(
 604                                    ix,
 605                                    connection_options,
 606                                    project,
 607                                    home_dir,
 608                                    window,
 609                                    cx,
 610                                    weak,
 611                                )
 612                            });
 613                        })
 614                        .ok();
 615                    Ok(())
 616                })
 617                .detach();
 618            })
 619        })
 620    }
 621
 622    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
 623        match &self.mode {
 624            Mode::Default(_) | Mode::ViewServerOptions(_) => {}
 625            Mode::ProjectPicker(_) => {}
 626            Mode::CreateRemoteServer(state) => {
 627                if let Some(prompt) = state.ssh_prompt.as_ref() {
 628                    prompt.update(cx, |prompt, cx| {
 629                        prompt.confirm(window, cx);
 630                    });
 631                    return;
 632                }
 633
 634                self.create_ssh_server(state.address_editor.clone(), window, cx);
 635            }
 636            Mode::EditNickname(state) => {
 637                let text = Some(state.editor.read(cx).text(cx)).filter(|text| !text.is_empty());
 638                let index = state.index;
 639                self.update_settings_file(cx, move |setting, _| {
 640                    if let Some(connections) = setting.ssh_connections.as_mut() {
 641                        if let Some(connection) = connections.get_mut(index) {
 642                            connection.nickname = text;
 643                        }
 644                    }
 645                });
 646                self.mode = Mode::default_mode(cx);
 647                self.focus_handle.focus(window);
 648            }
 649        }
 650    }
 651
 652    fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
 653        match &self.mode {
 654            Mode::Default(_) => cx.emit(DismissEvent),
 655            Mode::CreateRemoteServer(state) if state.ssh_prompt.is_some() => {
 656                let new_state = CreateRemoteServer::new(window, cx);
 657                let old_prompt = state.address_editor.read(cx).text(cx);
 658                new_state.address_editor.update(cx, |this, cx| {
 659                    this.set_text(old_prompt, window, cx);
 660                });
 661
 662                self.mode = Mode::CreateRemoteServer(new_state);
 663                cx.notify();
 664            }
 665            _ => {
 666                self.mode = Mode::default_mode(cx);
 667                self.focus_handle(cx).focus(window);
 668                cx.notify();
 669            }
 670        }
 671    }
 672
 673    fn render_ssh_connection(
 674        &mut self,
 675        ix: usize,
 676        ssh_server: RemoteEntry,
 677        window: &mut Window,
 678        cx: &mut Context<Self>,
 679    ) -> impl IntoElement {
 680        let connection = ssh_server.connection().into_owned();
 681        let (main_label, aux_label) = if let Some(nickname) = connection.nickname.clone() {
 682            let aux_label = SharedString::from(format!("({})", connection.host));
 683            (nickname.into(), Some(aux_label))
 684        } else {
 685            (connection.host.clone(), None)
 686        };
 687        v_flex()
 688            .w_full()
 689            .child(ListSeparator)
 690            .child(
 691                h_flex()
 692                    .group("ssh-server")
 693                    .w_full()
 694                    .pt_0p5()
 695                    .px_3()
 696                    .gap_1()
 697                    .overflow_hidden()
 698                    .child(
 699                        div().max_w_96().overflow_hidden().text_ellipsis().child(
 700                            Label::new(main_label)
 701                                .size(LabelSize::Small)
 702                                .color(Color::Muted),
 703                        ),
 704                    )
 705                    .children(
 706                        aux_label.map(|label| {
 707                            Label::new(label).size(LabelSize::Small).color(Color::Muted)
 708                        }),
 709                    ),
 710            )
 711            .child(match &ssh_server {
 712                RemoteEntry::Project {
 713                    open_folder,
 714                    projects,
 715                    configure,
 716                    connection,
 717                } => List::new()
 718                    .empty_message("No projects.")
 719                    .children(projects.iter().enumerate().map(|(pix, p)| {
 720                        v_flex().gap_0p5().child(self.render_ssh_project(
 721                            ix,
 722                            ssh_server.clone(),
 723                            pix,
 724                            p,
 725                            window,
 726                            cx,
 727                        ))
 728                    }))
 729                    .child(
 730                        h_flex()
 731                            .id(("new-remote-project-container", ix))
 732                            .track_focus(&open_folder.focus_handle)
 733                            .anchor_scroll(open_folder.scroll_anchor.clone())
 734                            .on_action(cx.listener({
 735                                let ssh_connection = connection.clone();
 736                                move |this, _: &menu::Confirm, window, cx| {
 737                                    this.create_ssh_project(ix, ssh_connection.clone(), window, cx);
 738                                }
 739                            }))
 740                            .child(
 741                                ListItem::new(("new-remote-project", ix))
 742                                    .toggle_state(
 743                                        open_folder.focus_handle.contains_focused(window, cx),
 744                                    )
 745                                    .inset(true)
 746                                    .spacing(ui::ListItemSpacing::Sparse)
 747                                    .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
 748                                    .child(Label::new("Open Folder"))
 749                                    .on_click(cx.listener({
 750                                        let ssh_connection = connection.clone();
 751                                        move |this, _, window, cx| {
 752                                            this.create_ssh_project(
 753                                                ix,
 754                                                ssh_connection.clone(),
 755                                                window,
 756                                                cx,
 757                                            );
 758                                        }
 759                                    })),
 760                            ),
 761                    )
 762                    .child(
 763                        h_flex()
 764                            .id(("server-options-container", ix))
 765                            .track_focus(&configure.focus_handle)
 766                            .anchor_scroll(configure.scroll_anchor.clone())
 767                            .on_action(cx.listener({
 768                                let ssh_connection = connection.clone();
 769                                move |this, _: &menu::Confirm, window, cx| {
 770                                    this.view_server_options(
 771                                        (ix, ssh_connection.clone()),
 772                                        window,
 773                                        cx,
 774                                    );
 775                                }
 776                            }))
 777                            .child(
 778                                ListItem::new(("server-options", ix))
 779                                    .toggle_state(
 780                                        configure.focus_handle.contains_focused(window, cx),
 781                                    )
 782                                    .inset(true)
 783                                    .spacing(ui::ListItemSpacing::Sparse)
 784                                    .start_slot(Icon::new(IconName::Settings).color(Color::Muted))
 785                                    .child(Label::new("View Server Options"))
 786                                    .on_click(cx.listener({
 787                                        let ssh_connection = connection.clone();
 788                                        move |this, _, window, cx| {
 789                                            this.view_server_options(
 790                                                (ix, ssh_connection.clone()),
 791                                                window,
 792                                                cx,
 793                                            );
 794                                        }
 795                                    })),
 796                            ),
 797                    ),
 798                RemoteEntry::SshConfig { open_folder, host } => List::new().child(
 799                    h_flex()
 800                        .id(("new-remote-project-container", ix))
 801                        .track_focus(&open_folder.focus_handle)
 802                        .anchor_scroll(open_folder.scroll_anchor.clone())
 803                        .on_action(cx.listener({
 804                            let ssh_connection = connection.clone();
 805                            let host = host.clone();
 806                            move |this, _: &menu::Confirm, window, cx| {
 807                                let new_ix = this.create_host_from_ssh_config(&host, cx);
 808                                this.create_ssh_project(new_ix, ssh_connection.clone(), window, cx);
 809                            }
 810                        }))
 811                        .child(
 812                            ListItem::new(("new-remote-project", ix))
 813                                .toggle_state(open_folder.focus_handle.contains_focused(window, cx))
 814                                .inset(true)
 815                                .spacing(ui::ListItemSpacing::Sparse)
 816                                .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
 817                                .child(Label::new("Open Folder"))
 818                                .on_click(cx.listener({
 819                                    let ssh_connection = connection.clone();
 820                                    let host = host.clone();
 821                                    move |this, _, window, cx| {
 822                                        let new_ix = this.create_host_from_ssh_config(&host, cx);
 823                                        this.create_ssh_project(
 824                                            new_ix,
 825                                            ssh_connection.clone(),
 826                                            window,
 827                                            cx,
 828                                        );
 829                                    }
 830                                })),
 831                        ),
 832                ),
 833            })
 834    }
 835
 836    fn render_ssh_project(
 837        &mut self,
 838        server_ix: usize,
 839        server: RemoteEntry,
 840        ix: usize,
 841        (navigation, project): &(NavigableEntry, SshProject),
 842        window: &mut Window,
 843        cx: &mut Context<Self>,
 844    ) -> impl IntoElement {
 845        let is_from_zed = server.is_from_zed();
 846        let element_id_base = SharedString::from(format!("remote-project-{server_ix}"));
 847        let container_element_id_base =
 848            SharedString::from(format!("remote-project-container-{element_id_base}"));
 849
 850        let callback = Rc::new({
 851            let project = project.clone();
 852            move |this: &mut Self, window: &mut Window, cx: &mut Context<Self>| {
 853                let Some(app_state) = this
 854                    .workspace
 855                    .update(cx, |workspace, _| workspace.app_state().clone())
 856                    .log_err()
 857                else {
 858                    return;
 859                };
 860                let project = project.clone();
 861                let server = server.connection().into_owned();
 862                cx.emit(DismissEvent);
 863                cx.spawn_in(window, async move |_, cx| {
 864                    let result = open_ssh_project(
 865                        server.into(),
 866                        project.paths.into_iter().map(PathBuf::from).collect(),
 867                        app_state,
 868                        OpenOptions::default(),
 869                        cx,
 870                    )
 871                    .await;
 872                    if let Err(e) = result {
 873                        log::error!("Failed to connect: {:?}", e);
 874                        cx.prompt(
 875                            gpui::PromptLevel::Critical,
 876                            "Failed to connect",
 877                            Some(&e.to_string()),
 878                            &["Ok"],
 879                        )
 880                        .await
 881                        .ok();
 882                    }
 883                })
 884                .detach();
 885            }
 886        });
 887
 888        div()
 889            .id((container_element_id_base, ix))
 890            .track_focus(&navigation.focus_handle)
 891            .anchor_scroll(navigation.scroll_anchor.clone())
 892            .on_action(cx.listener({
 893                let callback = callback.clone();
 894                move |this, _: &menu::Confirm, window, cx| {
 895                    callback(this, window, cx);
 896                }
 897            }))
 898            .child(
 899                ListItem::new((element_id_base, ix))
 900                    .toggle_state(navigation.focus_handle.contains_focused(window, cx))
 901                    .inset(true)
 902                    .spacing(ui::ListItemSpacing::Sparse)
 903                    .start_slot(
 904                        Icon::new(IconName::Folder)
 905                            .color(Color::Muted)
 906                            .size(IconSize::Small),
 907                    )
 908                    .child(Label::new(project.paths.join(", ")))
 909                    .on_click(cx.listener(move |this, _, window, cx| callback(this, window, cx)))
 910                    .when(is_from_zed, |server_list_item| {
 911                        server_list_item.end_hover_slot::<AnyElement>(Some(
 912                            div()
 913                                .mr_2()
 914                                .child({
 915                                    let project = project.clone();
 916                                    // Right-margin to offset it from the Scrollbar
 917                                    IconButton::new("remove-remote-project", IconName::TrashAlt)
 918                                        .icon_size(IconSize::Small)
 919                                        .shape(IconButtonShape::Square)
 920                                        .size(ButtonSize::Large)
 921                                        .tooltip(Tooltip::text("Delete Remote Project"))
 922                                        .on_click(cx.listener(move |this, _, _, cx| {
 923                                            this.delete_ssh_project(server_ix, &project, cx)
 924                                        }))
 925                                })
 926                                .into_any_element(),
 927                        ))
 928                    }),
 929            )
 930    }
 931
 932    fn update_settings_file(
 933        &mut self,
 934        cx: &mut Context<Self>,
 935        f: impl FnOnce(&mut RemoteSettingsContent, &App) + Send + Sync + 'static,
 936    ) {
 937        let Some(fs) = self
 938            .workspace
 939            .update(cx, |workspace, _| workspace.app_state().fs.clone())
 940            .log_err()
 941        else {
 942            return;
 943        };
 944        update_settings_file::<SshSettings>(fs, cx, move |setting, cx| f(setting, cx));
 945    }
 946
 947    fn delete_ssh_server(&mut self, server: usize, cx: &mut Context<Self>) {
 948        self.update_settings_file(cx, move |setting, _| {
 949            if let Some(connections) = setting.ssh_connections.as_mut() {
 950                connections.remove(server);
 951            }
 952        });
 953    }
 954
 955    fn delete_ssh_project(&mut self, server: usize, project: &SshProject, cx: &mut Context<Self>) {
 956        let project = project.clone();
 957        self.update_settings_file(cx, move |setting, _| {
 958            if let Some(server) = setting
 959                .ssh_connections
 960                .as_mut()
 961                .and_then(|connections| connections.get_mut(server))
 962            {
 963                server.projects.remove(&project);
 964            }
 965        });
 966    }
 967
 968    fn add_ssh_server(
 969        &mut self,
 970        connection_options: remote::SshConnectionOptions,
 971        cx: &mut Context<Self>,
 972    ) {
 973        self.update_settings_file(cx, move |setting, _| {
 974            setting
 975                .ssh_connections
 976                .get_or_insert(Default::default())
 977                .push(SshConnection {
 978                    host: SharedString::from(connection_options.host),
 979                    username: connection_options.username,
 980                    port: connection_options.port,
 981                    projects: BTreeSet::new(),
 982                    nickname: None,
 983                    args: connection_options.args.unwrap_or_default(),
 984                    upload_binary_over_ssh: None,
 985                    port_forwards: connection_options.port_forwards,
 986                })
 987        });
 988    }
 989
 990    fn render_create_remote_server(
 991        &self,
 992        state: &CreateRemoteServer,
 993        cx: &mut Context<Self>,
 994    ) -> impl IntoElement {
 995        let ssh_prompt = state.ssh_prompt.clone();
 996
 997        state.address_editor.update(cx, |editor, cx| {
 998            if editor.text(cx).is_empty() {
 999                editor.set_placeholder_text("ssh user@example -p 2222", cx);
1000            }
1001        });
1002
1003        let theme = cx.theme();
1004
1005        v_flex()
1006            .track_focus(&self.focus_handle(cx))
1007            .id("create-remote-server")
1008            .overflow_hidden()
1009            .size_full()
1010            .flex_1()
1011            .child(
1012                div()
1013                    .p_2()
1014                    .border_b_1()
1015                    .border_color(theme.colors().border_variant)
1016                    .child(state.address_editor.clone()),
1017            )
1018            .child(
1019                h_flex()
1020                    .bg(theme.colors().editor_background)
1021                    .rounded_b_sm()
1022                    .w_full()
1023                    .map(|this| {
1024                        if let Some(ssh_prompt) = ssh_prompt {
1025                            this.child(h_flex().w_full().child(ssh_prompt))
1026                        } else if let Some(address_error) = &state.address_error {
1027                            this.child(
1028                                h_flex().p_2().w_full().gap_2().child(
1029                                    Label::new(address_error.clone())
1030                                        .size(LabelSize::Small)
1031                                        .color(Color::Error),
1032                                ),
1033                            )
1034                        } else {
1035                            this.child(
1036                                h_flex()
1037                                    .p_2()
1038                                    .w_full()
1039                                    .gap_1()
1040                                    .child(
1041                                        Label::new(
1042                                            "Enter the command you use to SSH into this server.",
1043                                        )
1044                                        .color(Color::Muted)
1045                                        .size(LabelSize::Small),
1046                                    )
1047                                    .child(
1048                                        Button::new("learn-more", "Learn more…")
1049                                            .label_size(LabelSize::Small)
1050                                            .size(ButtonSize::None)
1051                                            .color(Color::Accent)
1052                                            .style(ButtonStyle::Transparent)
1053                                            .on_click(|_, _, cx| {
1054                                                cx.open_url(
1055                                                    "https://zed.dev/docs/remote-development",
1056                                                );
1057                                            }),
1058                                    ),
1059                            )
1060                        }
1061                    }),
1062            )
1063    }
1064
1065    fn render_view_options(
1066        &mut self,
1067        ViewServerOptionsState {
1068            server_index,
1069            connection,
1070            entries,
1071        }: ViewServerOptionsState,
1072        window: &mut Window,
1073        cx: &mut Context<Self>,
1074    ) -> impl IntoElement {
1075        let connection_string = connection.host.clone();
1076
1077        let mut view = Navigable::new(
1078            div()
1079                .track_focus(&self.focus_handle(cx))
1080                .size_full()
1081                .child(
1082                    SshConnectionHeader {
1083                        connection_string: connection_string.clone(),
1084                        paths: Default::default(),
1085                        nickname: connection.nickname.clone().map(|s| s.into()),
1086                    }
1087                    .render(window, cx),
1088                )
1089                .child(
1090                    v_flex()
1091                        .pb_1()
1092                        .child(ListSeparator)
1093                        .child({
1094                            let label = if connection.nickname.is_some() {
1095                                "Edit Nickname"
1096                            } else {
1097                                "Add Nickname to Server"
1098                            };
1099                            div()
1100                                .id("ssh-options-add-nickname")
1101                                .track_focus(&entries[0].focus_handle)
1102                                .on_action(cx.listener(
1103                                    move |this, _: &menu::Confirm, window, cx| {
1104                                        this.mode = Mode::EditNickname(EditNicknameState::new(
1105                                            server_index,
1106                                            window,
1107                                            cx,
1108                                        ));
1109                                        cx.notify();
1110                                    },
1111                                ))
1112                                .child(
1113                                    ListItem::new("add-nickname")
1114                                        .toggle_state(
1115                                            entries[0].focus_handle.contains_focused(window, cx),
1116                                        )
1117                                        .inset(true)
1118                                        .spacing(ui::ListItemSpacing::Sparse)
1119                                        .start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
1120                                        .child(Label::new(label))
1121                                        .on_click(cx.listener(move |this, _, window, cx| {
1122                                            this.mode = Mode::EditNickname(EditNicknameState::new(
1123                                                server_index,
1124                                                window,
1125                                                cx,
1126                                            ));
1127                                            cx.notify();
1128                                        })),
1129                                )
1130                        })
1131                        .child({
1132                            let workspace = self.workspace.clone();
1133                            fn callback(
1134                                workspace: WeakEntity<Workspace>,
1135                                connection_string: SharedString,
1136                                cx: &mut App,
1137                            ) {
1138                                cx.write_to_clipboard(ClipboardItem::new_string(
1139                                    connection_string.to_string(),
1140                                ));
1141                                workspace
1142                                    .update(cx, |this, cx| {
1143                                        struct SshServerAddressCopiedToClipboard;
1144                                        let notification = format!(
1145                                            "Copied server address ({}) to clipboard",
1146                                            connection_string
1147                                        );
1148
1149                                        this.show_toast(
1150                                            Toast::new(
1151                                                NotificationId::composite::<
1152                                                    SshServerAddressCopiedToClipboard,
1153                                                >(
1154                                                    connection_string.clone()
1155                                                ),
1156                                                notification,
1157                                            )
1158                                            .autohide(),
1159                                            cx,
1160                                        );
1161                                    })
1162                                    .ok();
1163                            }
1164                            div()
1165                                .id("ssh-options-copy-server-address")
1166                                .track_focus(&entries[1].focus_handle)
1167                                .on_action({
1168                                    let connection_string = connection_string.clone();
1169                                    let workspace = self.workspace.clone();
1170                                    move |_: &menu::Confirm, _, cx| {
1171                                        callback(workspace.clone(), connection_string.clone(), cx);
1172                                    }
1173                                })
1174                                .child(
1175                                    ListItem::new("copy-server-address")
1176                                        .toggle_state(
1177                                            entries[1].focus_handle.contains_focused(window, cx),
1178                                        )
1179                                        .inset(true)
1180                                        .spacing(ui::ListItemSpacing::Sparse)
1181                                        .start_slot(Icon::new(IconName::Copy).color(Color::Muted))
1182                                        .child(Label::new("Copy Server Address"))
1183                                        .end_hover_slot(
1184                                            Label::new(connection_string.clone())
1185                                                .color(Color::Muted),
1186                                        )
1187                                        .on_click({
1188                                            let connection_string = connection_string.clone();
1189                                            move |_, _, cx| {
1190                                                callback(
1191                                                    workspace.clone(),
1192                                                    connection_string.clone(),
1193                                                    cx,
1194                                                );
1195                                            }
1196                                        }),
1197                                )
1198                        })
1199                        .child({
1200                            fn remove_ssh_server(
1201                                remote_servers: Entity<RemoteServerProjects>,
1202                                index: usize,
1203                                connection_string: SharedString,
1204                                window: &mut Window,
1205                                cx: &mut App,
1206                            ) {
1207                                let prompt_message =
1208                                    format!("Remove server `{}`?", connection_string);
1209
1210                                let confirmation = window.prompt(
1211                                    PromptLevel::Warning,
1212                                    &prompt_message,
1213                                    None,
1214                                    &["Yes, remove it", "No, keep it"],
1215                                    cx,
1216                                );
1217
1218                                cx.spawn(async move |cx| {
1219                                    if confirmation.await.ok() == Some(0) {
1220                                        remote_servers
1221                                            .update(cx, |this, cx| {
1222                                                this.delete_ssh_server(index, cx);
1223                                            })
1224                                            .ok();
1225                                        remote_servers
1226                                            .update(cx, |this, cx| {
1227                                                this.mode = Mode::default_mode(cx);
1228                                                cx.notify();
1229                                            })
1230                                            .ok();
1231                                    }
1232                                    anyhow::Ok(())
1233                                })
1234                                .detach_and_log_err(cx);
1235                            }
1236                            div()
1237                                .id("ssh-options-copy-server-address")
1238                                .track_focus(&entries[2].focus_handle)
1239                                .on_action(cx.listener({
1240                                    let connection_string = connection_string.clone();
1241                                    move |_, _: &menu::Confirm, window, cx| {
1242                                        remove_ssh_server(
1243                                            cx.entity().clone(),
1244                                            server_index,
1245                                            connection_string.clone(),
1246                                            window,
1247                                            cx,
1248                                        );
1249                                        cx.focus_self(window);
1250                                    }
1251                                }))
1252                                .child(
1253                                    ListItem::new("remove-server")
1254                                        .toggle_state(
1255                                            entries[2].focus_handle.contains_focused(window, cx),
1256                                        )
1257                                        .inset(true)
1258                                        .spacing(ui::ListItemSpacing::Sparse)
1259                                        .start_slot(Icon::new(IconName::Trash).color(Color::Error))
1260                                        .child(Label::new("Remove Server").color(Color::Error))
1261                                        .on_click(cx.listener(move |_, _, window, cx| {
1262                                            remove_ssh_server(
1263                                                cx.entity().clone(),
1264                                                server_index,
1265                                                connection_string.clone(),
1266                                                window,
1267                                                cx,
1268                                            );
1269                                            cx.focus_self(window);
1270                                        })),
1271                                )
1272                        })
1273                        .child(ListSeparator)
1274                        .child({
1275                            div()
1276                                .id("ssh-options-copy-server-address")
1277                                .track_focus(&entries[3].focus_handle)
1278                                .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
1279                                    this.mode = Mode::default_mode(cx);
1280                                    cx.focus_self(window);
1281                                    cx.notify();
1282                                }))
1283                                .child(
1284                                    ListItem::new("go-back")
1285                                        .toggle_state(
1286                                            entries[3].focus_handle.contains_focused(window, cx),
1287                                        )
1288                                        .inset(true)
1289                                        .spacing(ui::ListItemSpacing::Sparse)
1290                                        .start_slot(
1291                                            Icon::new(IconName::ArrowLeft).color(Color::Muted),
1292                                        )
1293                                        .child(Label::new("Go Back"))
1294                                        .on_click(cx.listener(|this, _, window, cx| {
1295                                            this.mode = Mode::default_mode(cx);
1296                                            cx.focus_self(window);
1297                                            cx.notify()
1298                                        })),
1299                                )
1300                        }),
1301                )
1302                .into_any_element(),
1303        );
1304        for entry in entries {
1305            view = view.entry(entry);
1306        }
1307
1308        view.render(window, cx).into_any_element()
1309    }
1310
1311    fn render_edit_nickname(
1312        &self,
1313        state: &EditNicknameState,
1314        window: &mut Window,
1315        cx: &mut Context<Self>,
1316    ) -> impl IntoElement {
1317        let Some(connection) = SshSettings::get_global(cx)
1318            .ssh_connections()
1319            .nth(state.index)
1320        else {
1321            return v_flex()
1322                .id("ssh-edit-nickname")
1323                .track_focus(&self.focus_handle(cx));
1324        };
1325
1326        let connection_string = connection.host.clone();
1327        let nickname = connection.nickname.clone().map(|s| s.into());
1328
1329        v_flex()
1330            .id("ssh-edit-nickname")
1331            .track_focus(&self.focus_handle(cx))
1332            .child(
1333                SshConnectionHeader {
1334                    connection_string,
1335                    paths: Default::default(),
1336                    nickname,
1337                }
1338                .render(window, cx),
1339            )
1340            .child(
1341                h_flex()
1342                    .p_2()
1343                    .border_t_1()
1344                    .border_color(cx.theme().colors().border_variant)
1345                    .child(state.editor.clone()),
1346            )
1347    }
1348
1349    fn render_default(
1350        &mut self,
1351        mut state: DefaultState,
1352        window: &mut Window,
1353        cx: &mut Context<Self>,
1354    ) -> impl IntoElement {
1355        let ssh_settings = SshSettings::get_global(cx);
1356        let read_ssh_config = ssh_settings.read_ssh_config;
1357        if ssh_settings
1358            .ssh_connections
1359            .as_ref()
1360            .map_or(false, |connections| {
1361                state
1362                    .servers
1363                    .iter()
1364                    .filter_map(|server| match server {
1365                        RemoteEntry::Project { connection, .. } => Some(connection),
1366                        RemoteEntry::SshConfig { .. } => None,
1367                    })
1368                    .ne(connections.iter())
1369            })
1370        {
1371            self.mode = Mode::default_mode(cx);
1372            if let Mode::Default(new_state) = &self.mode {
1373                state = new_state.clone();
1374            }
1375        }
1376
1377        let mut extra_servers_from_config = if read_ssh_config {
1378            self.ssh_config_servers.clone()
1379        } else {
1380            BTreeSet::new()
1381        };
1382        let mut servers = state.servers.clone();
1383        for server in &servers {
1384            if let RemoteEntry::Project { connection, .. } = server {
1385                extra_servers_from_config.remove(&connection.host);
1386            }
1387        }
1388        servers.extend(
1389            extra_servers_from_config
1390                .into_iter()
1391                .map(|host| RemoteEntry::SshConfig {
1392                    open_folder: NavigableEntry::new(&state.handle, cx),
1393                    host,
1394                }),
1395        );
1396
1397        let scroll_state = state.scrollbar.parent_entity(&cx.entity());
1398        let connect_button = div()
1399            .id("ssh-connect-new-server-container")
1400            .track_focus(&state.add_new_server.focus_handle)
1401            .anchor_scroll(state.add_new_server.scroll_anchor.clone())
1402            .child(
1403                ListItem::new("register-remove-server-button")
1404                    .toggle_state(
1405                        state
1406                            .add_new_server
1407                            .focus_handle
1408                            .contains_focused(window, cx),
1409                    )
1410                    .inset(true)
1411                    .spacing(ui::ListItemSpacing::Sparse)
1412                    .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
1413                    .child(Label::new("Connect New Server"))
1414                    .on_click(cx.listener(|this, _, window, cx| {
1415                        let state = CreateRemoteServer::new(window, cx);
1416                        this.mode = Mode::CreateRemoteServer(state);
1417
1418                        cx.notify();
1419                    })),
1420            )
1421            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
1422                let state = CreateRemoteServer::new(window, cx);
1423                this.mode = Mode::CreateRemoteServer(state);
1424
1425                cx.notify();
1426            }));
1427
1428        let handle = &**scroll_state.scroll_handle() as &dyn Any;
1429        let Some(scroll_handle) = handle.downcast_ref::<ScrollHandle>() else {
1430            unreachable!()
1431        };
1432
1433        let mut modal_section = Navigable::new(
1434            v_flex()
1435                .track_focus(&self.focus_handle(cx))
1436                .id("ssh-server-list")
1437                .overflow_y_scroll()
1438                .track_scroll(&scroll_handle)
1439                .size_full()
1440                .child(connect_button)
1441                .child(
1442                    List::new()
1443                        .empty_message(
1444                            v_flex()
1445                                .child(
1446                                    div().px_3().child(
1447                                        Label::new("No remote servers registered yet.")
1448                                            .color(Color::Muted),
1449                                    ),
1450                                )
1451                                .into_any_element(),
1452                        )
1453                        .children(servers.iter().enumerate().map(|(ix, connection)| {
1454                            self.render_ssh_connection(ix, connection.clone(), window, cx)
1455                                .into_any_element()
1456                        })),
1457                )
1458                .into_any_element(),
1459        )
1460        .entry(state.add_new_server.clone());
1461
1462        for server in &servers {
1463            match server {
1464                RemoteEntry::Project {
1465                    open_folder,
1466                    projects,
1467                    configure,
1468                    ..
1469                } => {
1470                    for (navigation_state, _) in projects {
1471                        modal_section = modal_section.entry(navigation_state.clone());
1472                    }
1473                    modal_section = modal_section
1474                        .entry(open_folder.clone())
1475                        .entry(configure.clone());
1476                }
1477                RemoteEntry::SshConfig { open_folder, .. } => {
1478                    modal_section = modal_section.entry(open_folder.clone());
1479                }
1480            }
1481        }
1482        let mut modal_section = modal_section.render(window, cx).into_any_element();
1483
1484        Modal::new("remote-projects", None)
1485            .header(
1486                ModalHeader::new()
1487                    .child(Headline::new("Remote Projects").size(HeadlineSize::XSmall)),
1488            )
1489            .section(
1490                Section::new().padded(false).child(
1491                    v_flex()
1492                        .min_h(rems(20.))
1493                        .size_full()
1494                        .relative()
1495                        .child(ListSeparator)
1496                        .child(
1497                            canvas(
1498                                |bounds, window, cx| {
1499                                    modal_section.prepaint_as_root(
1500                                        bounds.origin,
1501                                        bounds.size.into(),
1502                                        window,
1503                                        cx,
1504                                    );
1505                                    modal_section
1506                                },
1507                                |_, mut modal_section, window, cx| {
1508                                    modal_section.paint(window, cx);
1509                                },
1510                            )
1511                            .size_full(),
1512                        )
1513                        .child(
1514                            div()
1515                                .occlude()
1516                                .h_full()
1517                                .absolute()
1518                                .top_1()
1519                                .bottom_1()
1520                                .right_1()
1521                                .w(px(8.))
1522                                .children(Scrollbar::vertical(scroll_state)),
1523                        ),
1524                ),
1525            )
1526            .into_any_element()
1527    }
1528
1529    fn create_host_from_ssh_config(
1530        &mut self,
1531        ssh_config_host: &SharedString,
1532        cx: &mut Context<'_, Self>,
1533    ) -> usize {
1534        let new_ix = Arc::new(AtomicUsize::new(0));
1535
1536        let update_new_ix = new_ix.clone();
1537        self.update_settings_file(cx, move |settings, _| {
1538            update_new_ix.store(
1539                settings
1540                    .ssh_connections
1541                    .as_ref()
1542                    .map_or(0, |connections| connections.len()),
1543                atomic::Ordering::Release,
1544            );
1545        });
1546
1547        self.add_ssh_server(
1548            SshConnectionOptions {
1549                host: ssh_config_host.to_string(),
1550                ..SshConnectionOptions::default()
1551            },
1552            cx,
1553        );
1554        self.mode = Mode::default_mode(cx);
1555        new_ix.load(atomic::Ordering::Acquire)
1556    }
1557}
1558
1559fn spawn_ssh_config_watch(fs: Arc<dyn Fs>, cx: &Context<RemoteServerProjects>) -> Task<()> {
1560    let mut user_ssh_config_watcher =
1561        watch_config_file(cx.background_executor(), fs.clone(), user_ssh_config_file());
1562    let mut global_ssh_config_watcher = watch_config_file(
1563        cx.background_executor(),
1564        fs,
1565        global_ssh_config_file().to_owned(),
1566    );
1567
1568    cx.spawn(async move |remote_server_projects, cx| {
1569        let mut global_hosts = BTreeSet::default();
1570        let mut user_hosts = BTreeSet::default();
1571        let mut running_receivers = 2;
1572
1573        loop {
1574            select! {
1575                new_global_file_contents = global_ssh_config_watcher.next().fuse() => {
1576                    match new_global_file_contents {
1577                        Some(new_global_file_contents) => {
1578                            global_hosts = parse_ssh_config_hosts(&new_global_file_contents);
1579                            if remote_server_projects.update(cx, |remote_server_projects, cx| {
1580                                remote_server_projects.ssh_config_servers = global_hosts.iter().chain(user_hosts.iter()).map(SharedString::from).collect();
1581                                cx.notify();
1582                            }).is_err() {
1583                                return;
1584                            }
1585                        },
1586                        None => {
1587                            running_receivers -= 1;
1588                            if running_receivers == 0 {
1589                                return;
1590                            }
1591                        }
1592                    }
1593                },
1594                new_user_file_contents = user_ssh_config_watcher.next().fuse() => {
1595                    match new_user_file_contents {
1596                        Some(new_user_file_contents) => {
1597                            user_hosts = parse_ssh_config_hosts(&new_user_file_contents);
1598                            if remote_server_projects.update(cx, |remote_server_projects, cx| {
1599                                remote_server_projects.ssh_config_servers = global_hosts.iter().chain(user_hosts.iter()).map(SharedString::from).collect();
1600                                cx.notify();
1601                            }).is_err() {
1602                                return;
1603                            }
1604                        },
1605                        None => {
1606                            running_receivers -= 1;
1607                            if running_receivers == 0 {
1608                                return;
1609                            }
1610                        }
1611                    }
1612                },
1613            }
1614        }
1615    })
1616}
1617
1618fn get_text(element: &Entity<Editor>, cx: &mut App) -> String {
1619    element.read(cx).text(cx).trim().to_string()
1620}
1621
1622impl ModalView for RemoteServerProjects {}
1623
1624impl Focusable for RemoteServerProjects {
1625    fn focus_handle(&self, cx: &App) -> FocusHandle {
1626        match &self.mode {
1627            Mode::ProjectPicker(picker) => picker.focus_handle(cx),
1628            _ => self.focus_handle.clone(),
1629        }
1630    }
1631}
1632
1633impl EventEmitter<DismissEvent> for RemoteServerProjects {}
1634
1635impl Render for RemoteServerProjects {
1636    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1637        div()
1638            .elevation_3(cx)
1639            .w(rems(34.))
1640            .key_context("RemoteServerModal")
1641            .on_action(cx.listener(Self::cancel))
1642            .on_action(cx.listener(Self::confirm))
1643            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
1644                this.focus_handle(cx).focus(window);
1645            }))
1646            .on_mouse_down_out(cx.listener(|this, _, _, cx| {
1647                if matches!(this.mode, Mode::Default(_)) {
1648                    cx.emit(DismissEvent)
1649                }
1650            }))
1651            .child(match &self.mode {
1652                Mode::Default(state) => self
1653                    .render_default(state.clone(), window, cx)
1654                    .into_any_element(),
1655                Mode::ViewServerOptions(state) => self
1656                    .render_view_options(state.clone(), window, cx)
1657                    .into_any_element(),
1658                Mode::ProjectPicker(element) => element.clone().into_any_element(),
1659                Mode::CreateRemoteServer(state) => self
1660                    .render_create_remote_server(state, cx)
1661                    .into_any_element(),
1662                Mode::EditNickname(state) => self
1663                    .render_edit_nickname(state, window, cx)
1664                    .into_any_element(),
1665            })
1666    }
1667}