dev_servers.rs

   1use std::collections::HashMap;
   2use std::path::PathBuf;
   3use std::time::Duration;
   4
   5use anyhow::anyhow;
   6use anyhow::Context;
   7use anyhow::Result;
   8use client::Client;
   9use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
  10use editor::Editor;
  11use gpui::AsyncWindowContext;
  12use gpui::PathPromptOptions;
  13use gpui::Subscription;
  14use gpui::Task;
  15use gpui::WeakView;
  16use gpui::{
  17    percentage, Animation, AnimationExt, AnyElement, AppContext, DismissEvent, EventEmitter,
  18    FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View, ViewContext,
  19};
  20use markdown::Markdown;
  21use markdown::MarkdownStyle;
  22use project::terminals::wrap_for_ssh;
  23use project::terminals::SshCommand;
  24use rpc::proto::RegenerateDevServerTokenResponse;
  25use rpc::{
  26    proto::{CreateDevServerResponse, DevServerStatus},
  27    ErrorCode, ErrorExt,
  28};
  29use settings::update_settings_file;
  30use settings::Settings;
  31use task::HideStrategy;
  32use task::RevealStrategy;
  33use task::SpawnInTerminal;
  34use terminal_view::terminal_panel::TerminalPanel;
  35use ui::ElevationIndex;
  36use ui::Section;
  37use ui::{
  38    prelude::*, Indicator, List, ListHeader, ListItem, Modal, ModalFooter, ModalHeader,
  39    RadioWithLabel, Tooltip,
  40};
  41use ui_input::{FieldLabelLayout, TextField};
  42use util::paths::PathWithPosition;
  43use util::ResultExt;
  44use workspace::notifications::NotifyResultExt;
  45use workspace::OpenOptions;
  46use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB};
  47
  48use crate::open_dev_server_project;
  49use crate::ssh_connections::connect_over_ssh;
  50use crate::ssh_connections::open_ssh_project;
  51use crate::ssh_connections::RemoteSettingsContent;
  52use crate::ssh_connections::SshConnection;
  53use crate::ssh_connections::SshConnectionModal;
  54use crate::ssh_connections::SshProject;
  55use crate::ssh_connections::SshPrompt;
  56use crate::ssh_connections::SshSettings;
  57use crate::OpenRemote;
  58
  59pub struct DevServerProjects {
  60    mode: Mode,
  61    focus_handle: FocusHandle,
  62    scroll_handle: ScrollHandle,
  63    dev_server_store: Model<dev_server_projects::Store>,
  64    workspace: WeakView<Workspace>,
  65    project_path_input: View<Editor>,
  66    dev_server_name_input: View<TextField>,
  67    markdown: View<Markdown>,
  68    _dev_server_subscription: Subscription,
  69}
  70
  71#[derive(Default)]
  72struct CreateDevServer {
  73    creating: Option<Task<Option<()>>>,
  74    dev_server_id: Option<DevServerId>,
  75    access_token: Option<String>,
  76    ssh_prompt: Option<View<SshPrompt>>,
  77    kind: NewServerKind,
  78}
  79
  80struct CreateDevServerProject {
  81    dev_server_id: DevServerId,
  82    creating: bool,
  83    _opening: Option<Subscription>,
  84}
  85
  86enum Mode {
  87    Default(Option<CreateDevServerProject>),
  88    CreateDevServer(CreateDevServer),
  89}
  90
  91#[derive(Default, PartialEq, Eq, Clone, Copy)]
  92enum NewServerKind {
  93    DirectSSH,
  94    #[default]
  95    LegacySSH,
  96    Manual,
  97}
  98
  99impl DevServerProjects {
 100    pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 101        workspace.register_action(|workspace, _: &OpenRemote, cx| {
 102            let handle = cx.view().downgrade();
 103            workspace.toggle_modal(cx, |cx| Self::new(cx, handle))
 104        });
 105    }
 106
 107    pub fn open(workspace: View<Workspace>, cx: &mut WindowContext) {
 108        workspace.update(cx, |workspace, cx| {
 109            let handle = cx.view().downgrade();
 110            workspace.toggle_modal(cx, |cx| Self::new(cx, handle))
 111        })
 112    }
 113
 114    pub fn new(cx: &mut ViewContext<Self>, workspace: WeakView<Workspace>) -> Self {
 115        let project_path_input = cx.new_view(|cx| {
 116            let mut editor = Editor::single_line(cx);
 117            editor.set_placeholder_text("Project path (~/work/zed, /workspace/zed, …)", cx);
 118            editor
 119        });
 120        let dev_server_name_input = cx.new_view(|cx| {
 121            TextField::new(cx, "Name", "192.168.0.1").with_label(FieldLabelLayout::Hidden)
 122        });
 123
 124        let focus_handle = cx.focus_handle();
 125        let dev_server_store = dev_server_projects::Store::global(cx);
 126
 127        let subscription = cx.observe(&dev_server_store, |_, _, cx| {
 128            cx.notify();
 129        });
 130
 131        let mut base_style = cx.text_style();
 132        base_style.refine(&gpui::TextStyleRefinement {
 133            color: Some(cx.theme().colors().editor_foreground),
 134            ..Default::default()
 135        });
 136
 137        let markdown_style = MarkdownStyle {
 138            base_text_style: base_style,
 139            code_block: gpui::StyleRefinement {
 140                text: Some(gpui::TextStyleRefinement {
 141                    font_family: Some("Zed Plex Mono".into()),
 142                    ..Default::default()
 143                }),
 144                ..Default::default()
 145            },
 146            link: gpui::TextStyleRefinement {
 147                color: Some(Color::Accent.color(cx)),
 148                ..Default::default()
 149            },
 150            syntax: cx.theme().syntax().clone(),
 151            selection_background_color: cx.theme().players().local().selection,
 152            ..Default::default()
 153        };
 154        let markdown =
 155            cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx, None));
 156
 157        Self {
 158            mode: Mode::Default(None),
 159            focus_handle,
 160            scroll_handle: ScrollHandle::new(),
 161            dev_server_store,
 162            project_path_input,
 163            dev_server_name_input,
 164            markdown,
 165            workspace,
 166            _dev_server_subscription: subscription,
 167        }
 168    }
 169
 170    pub fn create_dev_server_project(
 171        &mut self,
 172        dev_server_id: DevServerId,
 173        cx: &mut ViewContext<Self>,
 174    ) {
 175        let mut path = self.project_path_input.read(cx).text(cx).trim().to_string();
 176
 177        if path == "" {
 178            return;
 179        }
 180
 181        if !path.starts_with('/') && !path.starts_with('~') {
 182            path = format!("~/{}", path);
 183        }
 184
 185        if self
 186            .dev_server_store
 187            .read(cx)
 188            .projects_for_server(dev_server_id)
 189            .iter()
 190            .any(|p| p.paths.iter().any(|p| p == &path))
 191        {
 192            cx.spawn(|_, mut cx| async move {
 193                cx.prompt(
 194                    gpui::PromptLevel::Critical,
 195                    "Failed to create project",
 196                    Some(&format!("{} is already open on this dev server.", path)),
 197                    &["Ok"],
 198                )
 199                .await
 200            })
 201            .detach_and_log_err(cx);
 202            return;
 203        }
 204
 205        let create = {
 206            let path = path.clone();
 207            self.dev_server_store.update(cx, |store, cx| {
 208                store.create_dev_server_project(dev_server_id, path, cx)
 209            })
 210        };
 211
 212        cx.spawn(|this, mut cx| async move {
 213            let result = create.await;
 214            this.update(&mut cx, |this, cx| {
 215                if let Ok(result) = &result {
 216                    if let Some(dev_server_project_id) =
 217                        result.dev_server_project.as_ref().map(|p| p.id)
 218                    {
 219                        let subscription =
 220                            cx.observe(&this.dev_server_store, move |this, store, cx| {
 221                                if let Some(project_id) = store
 222                                    .read(cx)
 223                                    .dev_server_project(DevServerProjectId(dev_server_project_id))
 224                                    .and_then(|p| p.project_id)
 225                                {
 226                                    this.project_path_input.update(cx, |editor, cx| {
 227                                        editor.set_text("", cx);
 228                                    });
 229                                    this.mode = Mode::Default(None);
 230                                    if let Some(app_state) = AppState::global(cx).upgrade() {
 231                                        workspace::join_dev_server_project(
 232                                            DevServerProjectId(dev_server_project_id),
 233                                            project_id,
 234                                            app_state,
 235                                            None,
 236                                            cx,
 237                                        )
 238                                        .detach_and_prompt_err(
 239                                            "Could not join project",
 240                                            cx,
 241                                            |_, _| None,
 242                                        )
 243                                    }
 244                                }
 245                            });
 246
 247                        this.mode = Mode::Default(Some(CreateDevServerProject {
 248                            dev_server_id,
 249                            creating: true,
 250                            _opening: Some(subscription),
 251                        }));
 252                    }
 253                } else {
 254                    this.mode = Mode::Default(Some(CreateDevServerProject {
 255                        dev_server_id,
 256                        creating: false,
 257                        _opening: None,
 258                    }));
 259                }
 260            })
 261            .log_err();
 262            result
 263        })
 264        .detach_and_prompt_err("Failed to create project", cx, move |e, _| {
 265            match e.error_code() {
 266                ErrorCode::DevServerOffline => Some(
 267                    "The dev server is offline. Please log in and check it is connected."
 268                        .to_string(),
 269                ),
 270                ErrorCode::DevServerProjectPathDoesNotExist => {
 271                    Some(format!("The path `{}` does not exist on the server.", path))
 272                }
 273                _ => None,
 274            }
 275        });
 276
 277        self.mode = Mode::Default(Some(CreateDevServerProject {
 278            dev_server_id,
 279            creating: true,
 280            _opening: None,
 281        }));
 282    }
 283
 284    fn create_ssh_server(&mut self, cx: &mut ViewContext<Self>) {
 285        let host = get_text(&self.dev_server_name_input, cx);
 286        if host.is_empty() {
 287            return;
 288        }
 289
 290        let mut host = host.trim_start_matches("ssh ");
 291        let mut username: Option<String> = None;
 292        let mut port: Option<u16> = None;
 293
 294        if let Some((u, rest)) = host.split_once('@') {
 295            host = rest;
 296            username = Some(u.to_string());
 297        }
 298        if let Some((rest, p)) = host.split_once(':') {
 299            host = rest;
 300            port = p.parse().ok()
 301        }
 302
 303        if let Some((rest, p)) = host.split_once(" -p") {
 304            host = rest;
 305            port = p.trim().parse().ok()
 306        }
 307
 308        let connection_options = remote::SshConnectionOptions {
 309            host: host.to_string(),
 310            username,
 311            port,
 312            password: None,
 313        };
 314        let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));
 315        let connection = connect_over_ssh(connection_options.clone(), ssh_prompt.clone(), cx)
 316            .prompt_err("Failed to connect", cx, |_, _| None);
 317
 318        let creating = cx.spawn(move |this, mut cx| async move {
 319            match connection.await {
 320                Some(_) => this
 321                    .update(&mut cx, |this, cx| {
 322                        this.add_ssh_server(connection_options, cx);
 323                        this.mode = Mode::Default(None);
 324                        cx.notify()
 325                    })
 326                    .log_err(),
 327                None => this
 328                    .update(&mut cx, |this, cx| {
 329                        this.mode = Mode::CreateDevServer(CreateDevServer {
 330                            kind: NewServerKind::DirectSSH,
 331                            ..Default::default()
 332                        });
 333                        cx.notify()
 334                    })
 335                    .log_err(),
 336            };
 337            None
 338        });
 339        self.mode = Mode::CreateDevServer(CreateDevServer {
 340            kind: NewServerKind::DirectSSH,
 341            ssh_prompt: Some(ssh_prompt.clone()),
 342            creating: Some(creating),
 343            ..Default::default()
 344        });
 345    }
 346
 347    fn create_ssh_project(
 348        &mut self,
 349        ix: usize,
 350        ssh_connection: SshConnection,
 351        cx: &mut ViewContext<Self>,
 352    ) {
 353        let Some(workspace) = self.workspace.upgrade() else {
 354            return;
 355        };
 356
 357        let connection_options = ssh_connection.into();
 358        workspace.update(cx, |_, cx| {
 359            cx.defer(move |workspace, cx| {
 360                workspace.toggle_modal(cx, |cx| SshConnectionModal::new(&connection_options, cx));
 361                let prompt = workspace
 362                    .active_modal::<SshConnectionModal>(cx)
 363                    .unwrap()
 364                    .read(cx)
 365                    .prompt
 366                    .clone();
 367
 368                let connect = connect_over_ssh(connection_options, prompt, cx).prompt_err(
 369                    "Failed to connect",
 370                    cx,
 371                    |_, _| None,
 372                );
 373                cx.spawn(|workspace, mut cx| async move {
 374                    let Some(session) = connect.await else {
 375                        workspace
 376                            .update(&mut cx, |workspace, cx| {
 377                                let weak = cx.view().downgrade();
 378                                workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, weak));
 379                            })
 380                            .log_err();
 381                        return;
 382                    };
 383                    let Ok((app_state, project, paths)) =
 384                        workspace.update(&mut cx, |workspace, cx| {
 385                            let app_state = workspace.app_state().clone();
 386                            let project = project::Project::ssh(
 387                                session,
 388                                app_state.client.clone(),
 389                                app_state.node_runtime.clone(),
 390                                app_state.user_store.clone(),
 391                                app_state.languages.clone(),
 392                                app_state.fs.clone(),
 393                                cx,
 394                            );
 395                            let paths = workspace.prompt_for_open_path(
 396                                PathPromptOptions {
 397                                    files: true,
 398                                    directories: true,
 399                                    multiple: true,
 400                                },
 401                                project::DirectoryLister::Project(project.clone()),
 402                                cx,
 403                            );
 404                            (app_state, project, paths)
 405                        })
 406                    else {
 407                        return;
 408                    };
 409
 410                    let Ok(Some(paths)) = paths.await else {
 411                        workspace
 412                            .update(&mut cx, |workspace, cx| {
 413                                let weak = cx.view().downgrade();
 414                                workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, weak));
 415                            })
 416                            .log_err();
 417                        return;
 418                    };
 419
 420                    let Some(options) = cx
 421                        .update(|cx| (app_state.build_window_options)(None, cx))
 422                        .log_err()
 423                    else {
 424                        return;
 425                    };
 426
 427                    cx.open_window(options, |cx| {
 428                        cx.activate_window();
 429
 430                        let fs = app_state.fs.clone();
 431                        update_settings_file::<SshSettings>(fs, cx, {
 432                            let paths = paths
 433                                .iter()
 434                                .map(|path| path.to_string_lossy().to_string())
 435                                .collect();
 436                            move |setting, _| {
 437                                if let Some(server) = setting
 438                                    .ssh_connections
 439                                    .as_mut()
 440                                    .and_then(|connections| connections.get_mut(ix))
 441                                {
 442                                    server.projects.push(SshProject { paths })
 443                                }
 444                            }
 445                        });
 446
 447                        let tasks = paths
 448                            .into_iter()
 449                            .map(|path| {
 450                                project.update(cx, |project, cx| {
 451                                    project.find_or_create_worktree(&path, true, cx)
 452                                })
 453                            })
 454                            .collect::<Vec<_>>();
 455                        cx.spawn(|_| async move {
 456                            for task in tasks {
 457                                task.await?;
 458                            }
 459                            Ok(())
 460                        })
 461                        .detach_and_prompt_err(
 462                            "Failed to open path",
 463                            cx,
 464                            |_, _| None,
 465                        );
 466
 467                        cx.new_view(|cx| {
 468                            Workspace::new(None, project.clone(), app_state.clone(), cx)
 469                        })
 470                    })
 471                    .log_err();
 472                })
 473                .detach()
 474            })
 475        })
 476    }
 477
 478    fn create_or_update_dev_server(
 479        &mut self,
 480        kind: NewServerKind,
 481        existing_id: Option<DevServerId>,
 482        access_token: Option<String>,
 483        cx: &mut ViewContext<Self>,
 484    ) {
 485        let name = get_text(&self.dev_server_name_input, cx);
 486        if name.is_empty() {
 487            return;
 488        }
 489
 490        let manual_setup = match kind {
 491            NewServerKind::DirectSSH => unreachable!(),
 492            NewServerKind::LegacySSH => false,
 493            NewServerKind::Manual => true,
 494        };
 495
 496        let ssh_connection_string = if manual_setup {
 497            None
 498        } else if name.contains(' ') {
 499            Some(name.clone())
 500        } else {
 501            Some(format!("ssh {}", name))
 502        };
 503
 504        let dev_server = self.dev_server_store.update(cx, {
 505            let access_token = access_token.clone();
 506            |store, cx| {
 507                let ssh_connection_string = ssh_connection_string.clone();
 508                if let Some(dev_server_id) = existing_id {
 509                    let rename = store.rename_dev_server(
 510                        dev_server_id,
 511                        name.clone(),
 512                        ssh_connection_string,
 513                        cx,
 514                    );
 515                    let token = if let Some(access_token) = access_token {
 516                        Task::ready(Ok(RegenerateDevServerTokenResponse {
 517                            dev_server_id: dev_server_id.0,
 518                            access_token,
 519                        }))
 520                    } else {
 521                        store.regenerate_dev_server_token(dev_server_id, cx)
 522                    };
 523                    cx.spawn(|_, _| async move {
 524                        rename.await?;
 525                        let response = token.await?;
 526                        Ok(CreateDevServerResponse {
 527                            dev_server_id: dev_server_id.0,
 528                            name,
 529                            access_token: response.access_token,
 530                        })
 531                    })
 532                } else {
 533                    store.create_dev_server(name, ssh_connection_string.clone(), cx)
 534                }
 535            }
 536        });
 537
 538        let workspace = self.workspace.clone();
 539        let store = dev_server_projects::Store::global(cx);
 540
 541        let task = cx
 542            .spawn({
 543                |this, mut cx| async move {
 544                    let result = dev_server.await;
 545
 546                    match result {
 547                        Ok(dev_server) => {
 548                            if let Some(ssh_connection_string) = ssh_connection_string {
 549                                this.update(&mut cx, |this, cx| {
 550                                    if let Mode::CreateDevServer(CreateDevServer {
 551                                        access_token,
 552                                        dev_server_id,
 553                                        ..
 554                                    }) = &mut this.mode
 555                                    {
 556                                        access_token.replace(dev_server.access_token.clone());
 557                                        dev_server_id
 558                                            .replace(DevServerId(dev_server.dev_server_id));
 559                                    }
 560                                    cx.notify();
 561                                })?;
 562
 563                                spawn_ssh_task(
 564                                    workspace
 565                                        .upgrade()
 566                                        .ok_or_else(|| anyhow!("workspace dropped"))?,
 567                                    store,
 568                                    DevServerId(dev_server.dev_server_id),
 569                                    ssh_connection_string,
 570                                    dev_server.access_token.clone(),
 571                                    &mut cx,
 572                                )
 573                                .await
 574                                .log_err();
 575                            }
 576
 577                            this.update(&mut cx, |this, cx| {
 578                                this.focus_handle.focus(cx);
 579                                this.mode = Mode::CreateDevServer(CreateDevServer {
 580                                    dev_server_id: Some(DevServerId(dev_server.dev_server_id)),
 581                                    access_token: Some(dev_server.access_token),
 582                                    kind,
 583                                    ..Default::default()
 584                                });
 585                                cx.notify();
 586                            })?;
 587                            Ok(())
 588                        }
 589                        Err(e) => {
 590                            this.update(&mut cx, |this, cx| {
 591                                this.mode = Mode::CreateDevServer(CreateDevServer {
 592                                    dev_server_id: existing_id,
 593                                    access_token: None,
 594                                    kind,
 595                                    ..Default::default()
 596                                });
 597                                cx.notify()
 598                            })
 599                            .log_err();
 600
 601                            return Err(e);
 602                        }
 603                    }
 604                }
 605            })
 606            .prompt_err("Failed to create server", cx, |_, _| None);
 607
 608        self.mode = Mode::CreateDevServer(CreateDevServer {
 609            creating: Some(task),
 610            dev_server_id: existing_id,
 611            access_token,
 612            kind,
 613            ..Default::default()
 614        });
 615        cx.notify()
 616    }
 617
 618    fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) {
 619        let store = self.dev_server_store.read(cx);
 620        let prompt = if store.projects_for_server(id).is_empty()
 621            && store
 622                .dev_server(id)
 623                .is_some_and(|server| server.status == DevServerStatus::Offline)
 624        {
 625            None
 626        } else {
 627            Some(cx.prompt(
 628                gpui::PromptLevel::Warning,
 629                "Are you sure?",
 630                Some("This will delete the dev server and all of its remote projects."),
 631                &["Delete", "Cancel"],
 632            ))
 633        };
 634
 635        cx.spawn(|this, mut cx| async move {
 636            if let Some(prompt) = prompt {
 637                if prompt.await? != 0 {
 638                    return Ok(());
 639                }
 640            }
 641
 642            let project_ids: Vec<DevServerProjectId> = this.update(&mut cx, |this, cx| {
 643                this.dev_server_store.update(cx, |store, _| {
 644                    store
 645                        .projects_for_server(id)
 646                        .into_iter()
 647                        .map(|project| project.id)
 648                        .collect()
 649                })
 650            })?;
 651
 652            this.update(&mut cx, |this, cx| {
 653                this.dev_server_store
 654                    .update(cx, |store, cx| store.delete_dev_server(id, cx))
 655            })?
 656            .await?;
 657
 658            for id in project_ids {
 659                WORKSPACE_DB
 660                    .delete_workspace_by_dev_server_project_id(id)
 661                    .await
 662                    .log_err();
 663            }
 664            Ok(())
 665        })
 666        .detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None);
 667    }
 668
 669    fn delete_dev_server_project(&mut self, id: DevServerProjectId, cx: &mut ViewContext<Self>) {
 670        let answer = cx.prompt(
 671            gpui::PromptLevel::Warning,
 672            "Delete this project?",
 673            Some("This will delete the remote project. You can always re-add it later."),
 674            &["Delete", "Cancel"],
 675        );
 676
 677        cx.spawn(|this, mut cx| async move {
 678            let answer = answer.await?;
 679
 680            if answer != 0 {
 681                return Ok(());
 682            }
 683
 684            this.update(&mut cx, |this, cx| {
 685                this.dev_server_store
 686                    .update(cx, |store, cx| store.delete_dev_server_project(id, cx))
 687            })?
 688            .await?;
 689
 690            WORKSPACE_DB
 691                .delete_workspace_by_dev_server_project_id(id)
 692                .await
 693                .log_err();
 694
 695            Ok(())
 696        })
 697        .detach_and_prompt_err("Failed to delete dev server project", cx, |_, _| None);
 698    }
 699
 700    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
 701        match &self.mode {
 702            Mode::Default(None) => {}
 703            Mode::Default(Some(create_project)) => {
 704                self.create_dev_server_project(create_project.dev_server_id, cx);
 705            }
 706            Mode::CreateDevServer(state) => {
 707                if let Some(prompt) = state.ssh_prompt.as_ref() {
 708                    prompt.update(cx, |prompt, cx| {
 709                        prompt.confirm(cx);
 710                    });
 711                    return;
 712                }
 713                if state.kind == NewServerKind::DirectSSH {
 714                    self.create_ssh_server(cx);
 715                    return;
 716                }
 717                if state.creating.is_none() || state.dev_server_id.is_some() {
 718                    self.create_or_update_dev_server(
 719                        state.kind,
 720                        state.dev_server_id,
 721                        state.access_token.clone(),
 722                        cx,
 723                    );
 724                }
 725            }
 726        }
 727    }
 728
 729    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
 730        match &self.mode {
 731            Mode::Default(None) => cx.emit(DismissEvent),
 732            Mode::CreateDevServer(state) if state.ssh_prompt.is_some() => {
 733                self.mode = Mode::CreateDevServer(CreateDevServer {
 734                    kind: NewServerKind::DirectSSH,
 735                    ..Default::default()
 736                });
 737                cx.notify();
 738                return;
 739            }
 740            _ => {
 741                self.mode = Mode::Default(None);
 742                self.focus_handle(cx).focus(cx);
 743                cx.notify();
 744            }
 745        }
 746    }
 747
 748    fn render_dev_server(
 749        &mut self,
 750        dev_server: &DevServer,
 751        create_project: Option<bool>,
 752        cx: &mut ViewContext<Self>,
 753    ) -> impl IntoElement {
 754        let dev_server_id = dev_server.id;
 755        let status = dev_server.status;
 756        let dev_server_name = dev_server.name.clone();
 757        let kind = if dev_server.ssh_connection_string.is_some() {
 758            NewServerKind::LegacySSH
 759        } else {
 760            NewServerKind::Manual
 761        };
 762
 763        v_flex()
 764            .w_full()
 765            .child(
 766                h_flex().group("dev-server").justify_between().child(
 767                    h_flex()
 768                        .gap_2()
 769                        .child(
 770                            div()
 771                                .id(("status", dev_server.id.0))
 772                                .relative()
 773                                .child(Icon::new(IconName::Server).size(IconSize::Small))
 774                                .child(div().absolute().bottom_0().left(rems_from_px(8.0)).child(
 775                                    Indicator::dot().color(match status {
 776                                        DevServerStatus::Online => Color::Created,
 777                                        DevServerStatus::Offline => Color::Hidden,
 778                                    }),
 779                                ))
 780                                .tooltip(move |cx| {
 781                                    Tooltip::text(
 782                                        match status {
 783                                            DevServerStatus::Online => "Online",
 784                                            DevServerStatus::Offline => "Offline",
 785                                        },
 786                                        cx,
 787                                    )
 788                                }),
 789                        )
 790                        .child(
 791                            div()
 792                                .max_w(rems(26.))
 793                                .overflow_hidden()
 794                                .whitespace_nowrap()
 795                                .child(Label::new(dev_server_name.clone())),
 796                        )
 797                        .child(
 798                            h_flex()
 799                                .visible_on_hover("dev-server")
 800                                .gap_1()
 801                                .child(if dev_server.ssh_connection_string.is_some() {
 802                                    let dev_server = dev_server.clone();
 803                                    IconButton::new("reconnect-dev-server", IconName::ArrowCircle)
 804                                        .on_click(cx.listener(move |this, _, cx| {
 805                                            let Some(workspace) = this.workspace.upgrade() else {
 806                                                return;
 807                                            };
 808
 809                                            reconnect_to_dev_server(
 810                                                workspace,
 811                                                dev_server.clone(),
 812                                                cx,
 813                                            )
 814                                            .detach_and_prompt_err(
 815                                                "Failed to reconnect",
 816                                                cx,
 817                                                |_, _| None,
 818                                            );
 819                                        }))
 820                                        .tooltip(|cx| Tooltip::text("Reconnect", cx))
 821                                } else {
 822                                    IconButton::new("edit-dev-server", IconName::Pencil)
 823                                        .on_click(cx.listener(move |this, _, cx| {
 824                                            this.mode = Mode::CreateDevServer(CreateDevServer {
 825                                                dev_server_id: Some(dev_server_id),
 826                                                kind,
 827                                                ..Default::default()
 828                                            });
 829                                            let dev_server_name = dev_server_name.clone();
 830                                            this.dev_server_name_input.update(
 831                                                cx,
 832                                                move |input, cx| {
 833                                                    input.editor().update(cx, move |editor, cx| {
 834                                                        editor.set_text(dev_server_name, cx)
 835                                                    })
 836                                                },
 837                                            )
 838                                        }))
 839                                        .tooltip(|cx| Tooltip::text("Edit dev server", cx))
 840                                })
 841                                .child({
 842                                    let dev_server_id = dev_server.id;
 843                                    IconButton::new("remove-dev-server", IconName::Trash)
 844                                        .on_click(cx.listener(move |this, _, cx| {
 845                                            this.delete_dev_server(dev_server_id, cx)
 846                                        }))
 847                                        .tooltip(|cx| Tooltip::text("Remove dev server", cx))
 848                                }),
 849                        ),
 850                ),
 851            )
 852            .child(
 853                v_flex()
 854                    .w_full()
 855                    .bg(cx.theme().colors().background)
 856                    .border_1()
 857                    .border_color(cx.theme().colors().border_variant)
 858                    .rounded_md()
 859                    .my_1()
 860                    .py_0p5()
 861                    .px_3()
 862                    .child(
 863                        List::new()
 864                            .empty_message("No projects.")
 865                            .children(
 866                                self.dev_server_store
 867                                    .read(cx)
 868                                    .projects_for_server(dev_server.id)
 869                                    .iter()
 870                                    .map(|p| self.render_dev_server_project(p, cx)),
 871                            )
 872                            .when(
 873                                create_project.is_none()
 874                                    && dev_server.status == DevServerStatus::Online,
 875                                |el| {
 876                                    el.child(
 877                                        ListItem::new("new-remote_project")
 878                                            .start_slot(Icon::new(IconName::Plus))
 879                                            .child(Label::new("Open folder…"))
 880                                            .on_click(cx.listener(move |this, _, cx| {
 881                                                this.mode =
 882                                                    Mode::Default(Some(CreateDevServerProject {
 883                                                        dev_server_id,
 884                                                        creating: false,
 885                                                        _opening: None,
 886                                                    }));
 887                                                this.project_path_input
 888                                                    .read(cx)
 889                                                    .focus_handle(cx)
 890                                                    .focus(cx);
 891                                                cx.notify();
 892                                            })),
 893                                    )
 894                                },
 895                            )
 896                            .when_some(create_project, |el, creating| {
 897                                el.child(self.render_create_new_project(creating, cx))
 898                            }),
 899                    ),
 900            )
 901    }
 902
 903    fn render_ssh_connection(
 904        &mut self,
 905        ix: usize,
 906        ssh_connection: SshConnection,
 907        cx: &mut ViewContext<Self>,
 908    ) -> impl IntoElement {
 909        v_flex()
 910            .w_full()
 911            .child(
 912                h_flex().group("ssh-server").justify_between().child(
 913                    h_flex()
 914                        .gap_2()
 915                        .child(
 916                            div()
 917                                .id(("status", ix))
 918                                .relative()
 919                                .child(Icon::new(IconName::Server).size(IconSize::Small)),
 920                        )
 921                        .child(
 922                            div()
 923                                .max_w(rems(26.))
 924                                .overflow_hidden()
 925                                .whitespace_nowrap()
 926                                .child(Label::new(ssh_connection.host.clone())),
 927                        )
 928                        .child(h_flex().visible_on_hover("ssh-server").gap_1().child({
 929                            IconButton::new("remove-dev-server", IconName::Trash)
 930                                .on_click(
 931                                    cx.listener(move |this, _, cx| this.delete_ssh_server(ix, cx)),
 932                                )
 933                                .tooltip(|cx| Tooltip::text("Remove dev server", cx))
 934                        })),
 935                ),
 936            )
 937            .child(
 938                v_flex()
 939                    .w_full()
 940                    .bg(cx.theme().colors().background)
 941                    .border_1()
 942                    .border_color(cx.theme().colors().border_variant)
 943                    .rounded_md()
 944                    .my_1()
 945                    .py_0p5()
 946                    .px_3()
 947                    .child(
 948                        List::new()
 949                            .empty_message("No projects.")
 950                            .children(ssh_connection.projects.iter().enumerate().map(|(pix, p)| {
 951                                self.render_ssh_project(ix, &ssh_connection, pix, p, cx)
 952                            }))
 953                            .child(
 954                                ListItem::new("new-remote_project")
 955                                    .start_slot(Icon::new(IconName::Plus))
 956                                    .child(Label::new("Open folder…"))
 957                                    .on_click(cx.listener(move |this, _, cx| {
 958                                        this.create_ssh_project(ix, ssh_connection.clone(), cx);
 959                                    })),
 960                            ),
 961                    ),
 962            )
 963    }
 964
 965    fn render_ssh_project(
 966        &self,
 967        server_ix: usize,
 968        server: &SshConnection,
 969        ix: usize,
 970        project: &SshProject,
 971        cx: &ViewContext<Self>,
 972    ) -> impl IntoElement {
 973        let project = project.clone();
 974        let server = server.clone();
 975        ListItem::new(("remote-project", ix))
 976            .start_slot(Icon::new(IconName::FileTree))
 977            .child(Label::new(project.paths.join(", ")))
 978            .on_click(cx.listener(move |this, _, cx| {
 979                let Some(app_state) = this
 980                    .workspace
 981                    .update(cx, |workspace, _| workspace.app_state().clone())
 982                    .log_err()
 983                else {
 984                    return;
 985                };
 986                let project = project.clone();
 987                let server = server.clone();
 988                cx.spawn(|_, mut cx| async move {
 989                    let result = open_ssh_project(
 990                        server.into(),
 991                        project
 992                            .paths
 993                            .into_iter()
 994                            .map(|path| PathWithPosition::from_path(PathBuf::from(path)))
 995                            .collect(),
 996                        app_state,
 997                        OpenOptions::default(),
 998                        &mut cx,
 999                    )
1000                    .await;
1001                    if let Err(e) = result {
1002                        log::error!("Failed to connect: {:?}", e);
1003                        cx.prompt(
1004                            gpui::PromptLevel::Critical,
1005                            "Failed to connect",
1006                            Some(&e.to_string()),
1007                            &["Ok"],
1008                        )
1009                        .await
1010                        .ok();
1011                    }
1012                })
1013                .detach();
1014            }))
1015            .end_hover_slot::<AnyElement>(Some(
1016                IconButton::new("remove-remote-project", IconName::Trash)
1017                    .on_click(
1018                        cx.listener(move |this, _, cx| this.delete_ssh_project(server_ix, ix, cx)),
1019                    )
1020                    .tooltip(|cx| Tooltip::text("Delete remote project", cx))
1021                    .into_any_element(),
1022            ))
1023    }
1024
1025    fn update_settings_file(
1026        &mut self,
1027        cx: &mut ViewContext<Self>,
1028        f: impl FnOnce(&mut RemoteSettingsContent) + Send + Sync + 'static,
1029    ) {
1030        let Some(fs) = self
1031            .workspace
1032            .update(cx, |workspace, _| workspace.app_state().fs.clone())
1033            .log_err()
1034        else {
1035            return;
1036        };
1037        update_settings_file::<SshSettings>(fs, cx, move |setting, _| f(setting));
1038    }
1039
1040    fn delete_ssh_server(&mut self, server: usize, cx: &mut ViewContext<Self>) {
1041        self.update_settings_file(cx, move |setting| {
1042            if let Some(connections) = setting.ssh_connections.as_mut() {
1043                connections.remove(server);
1044            }
1045        });
1046    }
1047
1048    fn delete_ssh_project(&mut self, server: usize, project: usize, cx: &mut ViewContext<Self>) {
1049        self.update_settings_file(cx, move |setting| {
1050            if let Some(server) = setting
1051                .ssh_connections
1052                .as_mut()
1053                .and_then(|connections| connections.get_mut(server))
1054            {
1055                server.projects.remove(project);
1056            }
1057        });
1058    }
1059
1060    fn add_ssh_server(
1061        &mut self,
1062        connection_options: remote::SshConnectionOptions,
1063        cx: &mut ViewContext<Self>,
1064    ) {
1065        self.update_settings_file(cx, move |setting| {
1066            setting
1067                .ssh_connections
1068                .get_or_insert(Default::default())
1069                .push(SshConnection {
1070                    host: connection_options.host,
1071                    username: connection_options.username,
1072                    port: connection_options.port,
1073                    projects: vec![],
1074                })
1075        });
1076    }
1077
1078    fn render_create_new_project(
1079        &mut self,
1080        creating: bool,
1081        _: &mut ViewContext<Self>,
1082    ) -> impl IntoElement {
1083        ListItem::new("create-remote-project")
1084            .disabled(true)
1085            .start_slot(Icon::new(IconName::FileTree).color(Color::Muted))
1086            .child(self.project_path_input.clone())
1087            .child(div().w(IconSize::Medium.rems()).when(creating, |el| {
1088                el.child(
1089                    Icon::new(IconName::ArrowCircle)
1090                        .size(IconSize::Medium)
1091                        .with_animation(
1092                            "arrow-circle",
1093                            Animation::new(Duration::from_secs(2)).repeat(),
1094                            |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
1095                        ),
1096                )
1097            }))
1098    }
1099
1100    fn render_dev_server_project(
1101        &mut self,
1102        project: &DevServerProject,
1103        cx: &mut ViewContext<Self>,
1104    ) -> impl IntoElement {
1105        let dev_server_project_id = project.id;
1106        let project_id = project.project_id;
1107        let is_online = project_id.is_some();
1108
1109        ListItem::new(("remote-project", dev_server_project_id.0))
1110            .start_slot(Icon::new(IconName::FileTree).when(!is_online, |icon| icon.color(Color::Muted)))
1111            .child(
1112                    Label::new(project.paths.join(", "))
1113            )
1114            .on_click(cx.listener(move |_, _, cx| {
1115                if let Some(project_id) = project_id {
1116                    if let Some(app_state) = AppState::global(cx).upgrade() {
1117                        workspace::join_dev_server_project(dev_server_project_id, project_id, app_state, None, cx)
1118                            .detach_and_prompt_err("Could not join project", cx, |_, _| None)
1119                    }
1120                } else {
1121                    cx.spawn(|_, mut cx| async move {
1122                        cx.prompt(gpui::PromptLevel::Critical, "This project is offline", Some("The `zed` instance running on this dev server is not connected. You will have to restart it."), &["Ok"]).await.log_err();
1123                    }).detach();
1124                }
1125            }))
1126            .end_hover_slot::<AnyElement>(Some(IconButton::new("remove-remote-project", IconName::Trash)
1127                .on_click(cx.listener(move |this, _, cx| {
1128                    this.delete_dev_server_project(dev_server_project_id, cx)
1129                }))
1130                .tooltip(|cx| Tooltip::text("Delete remote project", cx)).into_any_element()))
1131    }
1132
1133    fn render_create_dev_server(
1134        &self,
1135        state: &CreateDevServer,
1136        cx: &mut ViewContext<Self>,
1137    ) -> impl IntoElement {
1138        let creating = state.creating.is_some();
1139        let dev_server_id = state.dev_server_id;
1140        let access_token = state.access_token.clone();
1141        let ssh_prompt = state.ssh_prompt.clone();
1142        let use_direct_ssh = SshSettings::get_global(cx).use_direct_ssh();
1143
1144        let mut kind = state.kind;
1145        if use_direct_ssh && kind == NewServerKind::LegacySSH {
1146            kind = NewServerKind::DirectSSH;
1147        }
1148
1149        let status = dev_server_id
1150            .map(|id| self.dev_server_store.read(cx).dev_server_status(id))
1151            .unwrap_or_default();
1152
1153        let name = self.dev_server_name_input.update(cx, |input, cx| {
1154            input.editor().update(cx, |editor, cx| {
1155                if editor.text(cx).is_empty() {
1156                    match kind {
1157                        NewServerKind::DirectSSH => editor.set_placeholder_text("ssh host", cx),
1158                        NewServerKind::LegacySSH => editor.set_placeholder_text("ssh host", cx),
1159                        NewServerKind::Manual => editor.set_placeholder_text("example-host", cx),
1160                    }
1161                }
1162                editor.text(cx)
1163            })
1164        });
1165
1166        const MANUAL_SETUP_MESSAGE: &str = "Click create to generate a token for this server. The next step will provide instructions for setting zed up on that machine.";
1167        const SSH_SETUP_MESSAGE: &str =
1168            "Enter the command you use to ssh into this server.\nFor example: `ssh me@my.server` or `ssh me@secret-box:2222`.";
1169
1170        Modal::new("create-dev-server", Some(self.scroll_handle.clone()))
1171            .header(
1172                ModalHeader::new()
1173                    .headline("Create Dev Server")
1174                    .show_back_button(true),
1175            )
1176            .section(
1177                Section::new()
1178                    .header(if kind == NewServerKind::Manual {
1179                        "Server Name".into()
1180                    } else {
1181                        "SSH arguments".into()
1182                    })
1183                    .child(
1184                        div()
1185                            .max_w(rems(16.))
1186                            .child(self.dev_server_name_input.clone()),
1187                    ),
1188            )
1189            .section(
1190                Section::new_contained()
1191                    .header("Connection Method".into())
1192                    .child(
1193                        v_flex()
1194                            .w_full()
1195                            .gap_y(Spacing::Large.rems(cx))
1196                            .when(ssh_prompt.is_none(), |el| {
1197                                el.child(
1198                                    v_flex()
1199                                        .when(use_direct_ssh, |el| {
1200                                            el.child(RadioWithLabel::new(
1201                                                "use-server-name-in-ssh",
1202                                                Label::new("Connect via SSH (default)"),
1203                                                NewServerKind::DirectSSH == kind,
1204                                                cx.listener({
1205                                                    move |this, _, cx| {
1206                                                        if let Mode::CreateDevServer(
1207                                                            CreateDevServer { kind, .. },
1208                                                        ) = &mut this.mode
1209                                                        {
1210                                                            *kind = NewServerKind::DirectSSH;
1211                                                        }
1212                                                        cx.notify()
1213                                                    }
1214                                                }),
1215                                            ))
1216                                        })
1217                                        .when(!use_direct_ssh, |el| {
1218                                            el.child(RadioWithLabel::new(
1219                                                "use-server-name-in-ssh",
1220                                                Label::new("Configure over SSH (default)"),
1221                                                kind == NewServerKind::LegacySSH,
1222                                                cx.listener({
1223                                                    move |this, _, cx| {
1224                                                        if let Mode::CreateDevServer(
1225                                                            CreateDevServer { kind, .. },
1226                                                        ) = &mut this.mode
1227                                                        {
1228                                                            *kind = NewServerKind::LegacySSH;
1229                                                        }
1230                                                        cx.notify()
1231                                                    }
1232                                                }),
1233                                            ))
1234                                        })
1235                                        .child(RadioWithLabel::new(
1236                                            "use-server-name-in-ssh",
1237                                            Label::new("Configure manually"),
1238                                            kind == NewServerKind::Manual,
1239                                            cx.listener({
1240                                                move |this, _, cx| {
1241                                                    if let Mode::CreateDevServer(
1242                                                        CreateDevServer { kind, .. },
1243                                                    ) = &mut this.mode
1244                                                    {
1245                                                        *kind = NewServerKind::Manual;
1246                                                    }
1247                                                    cx.notify()
1248                                                }
1249                                            }),
1250                                        )),
1251                                )
1252                            })
1253                            .when(dev_server_id.is_none() && ssh_prompt.is_none(), |el| {
1254                                el.child(
1255                                    if kind == NewServerKind::Manual {
1256                                        Label::new(MANUAL_SETUP_MESSAGE)
1257                                    } else {
1258                                        Label::new(SSH_SETUP_MESSAGE)
1259                                    }
1260                                    .size(LabelSize::Small)
1261                                    .color(Color::Muted),
1262                                )
1263                            })
1264                            .when_some(ssh_prompt, |el, ssh_prompt| el.child(ssh_prompt))
1265                            .when(dev_server_id.is_some() && access_token.is_none(), |el| {
1266                                el.child(
1267                                    if kind == NewServerKind::Manual {
1268                                        Label::new(
1269                                            "Note: updating the dev server generate a new token",
1270                                        )
1271                                    } else {
1272                                        Label::new(SSH_SETUP_MESSAGE)
1273                                    }
1274                                    .size(LabelSize::Small)
1275                                    .color(Color::Muted),
1276                                )
1277                            })
1278                            .when_some(access_token.clone(), {
1279                                |el, access_token| {
1280                                    el.child(self.render_dev_server_token_creating(
1281                                        access_token,
1282                                        name,
1283                                        kind,
1284                                        status,
1285                                        creating,
1286                                        cx,
1287                                    ))
1288                                }
1289                            }),
1290                    ),
1291            )
1292            .footer(
1293                ModalFooter::new().end_slot(if status == DevServerStatus::Online {
1294                    Button::new("create-dev-server", "Done")
1295                        .style(ButtonStyle::Filled)
1296                        .layer(ElevationIndex::ModalSurface)
1297                        .on_click(cx.listener(move |this, _, cx| {
1298                            cx.focus(&this.focus_handle);
1299                            this.mode = Mode::Default(None);
1300                            cx.notify();
1301                        }))
1302                } else {
1303                    Button::new(
1304                        "create-dev-server",
1305                        if kind == NewServerKind::Manual {
1306                            if dev_server_id.is_some() {
1307                                "Update"
1308                            } else {
1309                                "Create"
1310                            }
1311                        } else {
1312                            if dev_server_id.is_some() {
1313                                "Reconnect"
1314                            } else {
1315                                "Connect"
1316                            }
1317                        },
1318                    )
1319                    .style(ButtonStyle::Filled)
1320                    .layer(ElevationIndex::ModalSurface)
1321                    .disabled(creating && dev_server_id.is_none())
1322                    .on_click(cx.listener({
1323                        let access_token = access_token.clone();
1324                        move |this, _, cx| {
1325                            if kind == NewServerKind::DirectSSH {
1326                                this.create_ssh_server(cx);
1327                                return;
1328                            }
1329                            this.create_or_update_dev_server(
1330                                kind,
1331                                dev_server_id,
1332                                access_token.clone(),
1333                                cx,
1334                            );
1335                        }
1336                    }))
1337                }),
1338            )
1339    }
1340
1341    fn render_dev_server_token_creating(
1342        &self,
1343        access_token: String,
1344        dev_server_name: String,
1345        kind: NewServerKind,
1346        status: DevServerStatus,
1347        creating: bool,
1348        cx: &mut ViewContext<Self>,
1349    ) -> Div {
1350        self.markdown.update(cx, |markdown, cx| {
1351            if kind == NewServerKind::Manual {
1352                markdown.reset(format!("Please log into '{}'. If you don't yet have zed installed, run:\n```\ncurl https://zed.dev/install.sh | bash\n```\nThen to start zed in headless mode:\n```\nzed --dev-server-token {}\n```", dev_server_name, access_token), cx);
1353            } else {
1354                markdown.reset("Please wait while we connect over SSH.\n\nIf you run into problems, please [file a bug](https://github.com/zed-industries/zed), and in the meantime try using manual setup.".to_string(), cx);
1355            }
1356        });
1357
1358        v_flex()
1359            .pl_2()
1360            .pt_2()
1361            .gap_2()
1362            .child(v_flex().w_full().text_sm().child(self.markdown.clone()))
1363            .map(|el| {
1364                if status == DevServerStatus::Offline && kind != NewServerKind::Manual && !creating
1365                {
1366                    el.child(
1367                        h_flex()
1368                            .gap_2()
1369                            .child(Icon::new(IconName::Disconnected).size(IconSize::Medium))
1370                            .child(Label::new("Not connected")),
1371                    )
1372                } else if status == DevServerStatus::Offline {
1373                    el.child(Self::render_loading_spinner("Waiting for connection…"))
1374                } else {
1375                    el.child(Label::new("🎊 Connection established!"))
1376                }
1377            })
1378    }
1379
1380    fn render_loading_spinner(label: impl Into<SharedString>) -> Div {
1381        h_flex()
1382            .gap_2()
1383            .child(
1384                Icon::new(IconName::ArrowCircle)
1385                    .size(IconSize::Medium)
1386                    .with_animation(
1387                        "arrow-circle",
1388                        Animation::new(Duration::from_secs(2)).repeat(),
1389                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
1390                    ),
1391            )
1392            .child(Label::new(label))
1393    }
1394
1395    fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1396        let dev_servers = self.dev_server_store.read(cx).dev_servers();
1397        let ssh_connections = SshSettings::get_global(cx)
1398            .ssh_connections()
1399            .collect::<Vec<_>>();
1400
1401        let Mode::Default(create_dev_server_project) = &self.mode else {
1402            unreachable!()
1403        };
1404
1405        let mut is_creating = None;
1406        let mut creating_dev_server = None;
1407        if let Some(CreateDevServerProject {
1408            creating,
1409            dev_server_id,
1410            ..
1411        }) = create_dev_server_project
1412        {
1413            is_creating = Some(*creating);
1414            creating_dev_server = Some(*dev_server_id);
1415        };
1416        let is_signed_out = Client::global(cx).status().borrow().is_signed_out();
1417
1418        Modal::new("remote-projects", Some(self.scroll_handle.clone()))
1419            .header(
1420                ModalHeader::new()
1421                    .show_dismiss_button(true)
1422                    .child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::Small)),
1423            )
1424            .when(is_signed_out, |modal| {
1425                modal
1426                    .section(Section::new().child(v_flex().mb_4().child(Label::new(
1427                        "You are not currently signed in to Zed. Currently the remote development features are only available to signed in users. Please sign in to continue.",
1428                    ))))
1429                    .footer(
1430                        ModalFooter::new().end_slot(
1431                            Button::new("sign_in", "Sign in")
1432                                .icon(IconName::Github)
1433                                .icon_position(IconPosition::Start)
1434                                .style(ButtonStyle::Filled)
1435                                .full_width()
1436                                .on_click(cx.listener(|_, _, cx| {
1437                                    let client = Client::global(cx).clone();
1438                                    cx.spawn(|_, mut cx| async move {
1439                                        client
1440                                            .authenticate_and_connect(true, &cx)
1441                                            .await
1442                                            .notify_async_err(&mut cx);
1443                                    })
1444                                    .detach();
1445                                    cx.emit(gpui::DismissEvent);
1446                                })),
1447                        ),
1448                    )
1449            })
1450            .when(!is_signed_out, |modal| {
1451                modal.section(
1452                    Section::new().child(
1453                        div().mb_4().child(
1454                            List::new()
1455                                .empty_message("No dev servers registered.")
1456                                .header(Some(
1457                                    ListHeader::new("Connections").end_slot(
1458                                        Button::new("register-dev-server-button", "Connect")
1459                                            .icon(IconName::Plus)
1460                                            .icon_position(IconPosition::Start)
1461                                            .tooltip(|cx| {
1462                                                Tooltip::text("Connect to a new server", cx)
1463                                            })
1464                                            .on_click(cx.listener(|this, _, cx| {
1465                                                this.mode = Mode::CreateDevServer(
1466                                                    CreateDevServer {
1467                                                        kind: if SshSettings::get_global(cx).use_direct_ssh() { NewServerKind::DirectSSH } else { NewServerKind::LegacySSH },
1468                                                        ..Default::default()
1469                                                    }
1470                                                );
1471                                                this.dev_server_name_input.update(
1472                                                    cx,
1473                                                    |text_field, cx| {
1474                                                        text_field.editor().update(
1475                                                            cx,
1476                                                            |editor, cx| {
1477                                                                editor.set_text("", cx);
1478                                                            },
1479                                                        );
1480                                                    },
1481                                                );
1482                                                cx.notify();
1483                                            })),
1484                                    ),
1485                                ))
1486                                .children(ssh_connections.iter().cloned().enumerate().map(|(ix, connection)| {
1487                                    self.render_ssh_connection(ix, connection, cx)
1488                                        .into_any_element()
1489                                }))
1490                                .children(dev_servers.iter().map(|dev_server| {
1491                                    let creating = if creating_dev_server == Some(dev_server.id) {
1492                                        is_creating
1493                                    } else {
1494                                        None
1495                                    };
1496                                    self.render_dev_server(dev_server, creating, cx)
1497                                        .into_any_element()
1498                                })),
1499                        ),
1500                    ),
1501                )
1502            })
1503    }
1504}
1505
1506fn get_text(element: &View<TextField>, cx: &mut WindowContext) -> String {
1507    element
1508        .read(cx)
1509        .editor()
1510        .read(cx)
1511        .text(cx)
1512        .trim()
1513        .to_string()
1514}
1515
1516impl ModalView for DevServerProjects {}
1517
1518impl FocusableView for DevServerProjects {
1519    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
1520        self.focus_handle.clone()
1521    }
1522}
1523
1524impl EventEmitter<DismissEvent> for DevServerProjects {}
1525
1526impl Render for DevServerProjects {
1527    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1528        div()
1529            .track_focus(&self.focus_handle)
1530            .elevation_3(cx)
1531            .key_context("DevServerModal")
1532            .on_action(cx.listener(Self::cancel))
1533            .on_action(cx.listener(Self::confirm))
1534            .capture_any_mouse_down(cx.listener(|this, _, cx| {
1535                this.focus_handle(cx).focus(cx);
1536            }))
1537            .on_mouse_down_out(cx.listener(|this, _, cx| {
1538                if matches!(this.mode, Mode::Default(None)) {
1539                    cx.emit(DismissEvent)
1540                }
1541            }))
1542            .w(rems(34.))
1543            .max_h(rems(40.))
1544            .child(match &self.mode {
1545                Mode::Default(_) => self.render_default(cx).into_any_element(),
1546                Mode::CreateDevServer(state) => {
1547                    self.render_create_dev_server(state, cx).into_any_element()
1548                }
1549            })
1550    }
1551}
1552
1553pub fn reconnect_to_dev_server_project(
1554    workspace: View<Workspace>,
1555    dev_server: DevServer,
1556    dev_server_project_id: DevServerProjectId,
1557    replace_current_window: bool,
1558    cx: &mut WindowContext,
1559) -> Task<Result<()>> {
1560    let store = dev_server_projects::Store::global(cx);
1561    let reconnect = reconnect_to_dev_server(workspace.clone(), dev_server, cx);
1562    cx.spawn(|mut cx| async move {
1563        reconnect.await?;
1564
1565        cx.background_executor()
1566            .timer(Duration::from_millis(1000))
1567            .await;
1568
1569        if let Some(project_id) = store.update(&mut cx, |store, _| {
1570            store
1571                .dev_server_project(dev_server_project_id)
1572                .and_then(|p| p.project_id)
1573        })? {
1574            workspace
1575                .update(&mut cx, move |_, cx| {
1576                    open_dev_server_project(
1577                        replace_current_window,
1578                        dev_server_project_id,
1579                        project_id,
1580                        cx,
1581                    )
1582                })?
1583                .await?;
1584        }
1585
1586        Ok(())
1587    })
1588}
1589
1590pub fn reconnect_to_dev_server(
1591    workspace: View<Workspace>,
1592    dev_server: DevServer,
1593    cx: &mut WindowContext,
1594) -> Task<Result<()>> {
1595    let Some(ssh_connection_string) = dev_server.ssh_connection_string else {
1596        return Task::ready(Err(anyhow!("can't reconnect, no ssh_connection_string")));
1597    };
1598    let dev_server_store = dev_server_projects::Store::global(cx);
1599    let get_access_token = dev_server_store.update(cx, |store, cx| {
1600        store.regenerate_dev_server_token(dev_server.id, cx)
1601    });
1602
1603    cx.spawn(|mut cx| async move {
1604        let access_token = get_access_token.await?.access_token;
1605
1606        spawn_ssh_task(
1607            workspace,
1608            dev_server_store,
1609            dev_server.id,
1610            ssh_connection_string.to_string(),
1611            access_token,
1612            &mut cx,
1613        )
1614        .await
1615    })
1616}
1617
1618pub async fn spawn_ssh_task(
1619    workspace: View<Workspace>,
1620    dev_server_store: Model<dev_server_projects::Store>,
1621    dev_server_id: DevServerId,
1622    ssh_connection_string: String,
1623    access_token: String,
1624    cx: &mut AsyncWindowContext,
1625) -> Result<()> {
1626    let terminal_panel = workspace
1627        .update(cx, |workspace, cx| workspace.panel::<TerminalPanel>(cx))
1628        .ok()
1629        .flatten()
1630        .with_context(|| anyhow!("No terminal panel"))?;
1631
1632    let command = "sh".to_string();
1633    let args = vec![
1634        "-x".to_string(),
1635        "-c".to_string(),
1636        format!(
1637            r#"~/.local/bin/zed -v >/dev/stderr || (curl -f https://zed.dev/install.sh || wget -qO- https://zed.dev/install.sh) | sh && ZED_HEADLESS=1 ~/.local/bin/zed --dev-server-token {}"#,
1638            access_token
1639        ),
1640    ];
1641
1642    let ssh_connection_string = ssh_connection_string.to_string();
1643    let (command, args) = wrap_for_ssh(
1644        &SshCommand::DevServer(ssh_connection_string.clone()),
1645        Some((&command, &args)),
1646        None,
1647        HashMap::default(),
1648        None,
1649    );
1650
1651    let terminal = terminal_panel
1652        .update(cx, |terminal_panel, cx| {
1653            terminal_panel.spawn_in_new_terminal(
1654                SpawnInTerminal {
1655                    id: task::TaskId("ssh-remote".into()),
1656                    full_label: "Install zed over ssh".into(),
1657                    label: "Install zed over ssh".into(),
1658                    command,
1659                    args,
1660                    command_label: ssh_connection_string.clone(),
1661                    cwd: None,
1662                    use_new_terminal: true,
1663                    allow_concurrent_runs: false,
1664                    reveal: RevealStrategy::Always,
1665                    hide: HideStrategy::Never,
1666                    env: Default::default(),
1667                    shell: Default::default(),
1668                },
1669                cx,
1670            )
1671        })?
1672        .await?;
1673
1674    terminal
1675        .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
1676        .await;
1677
1678    // There's a race-condition between the task completing successfully, and the server sending us the online status. Make it less likely we'll show the error state.
1679    if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
1680        == DevServerStatus::Offline
1681    {
1682        cx.background_executor()
1683            .timer(Duration::from_millis(200))
1684            .await
1685    }
1686
1687    if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
1688        == DevServerStatus::Offline
1689    {
1690        return Err(anyhow!("couldn't reconnect"))?;
1691    }
1692
1693    Ok(())
1694}