dev_servers.rs

   1use std::time::Duration;
   2
   3use anyhow::anyhow;
   4use anyhow::Context;
   5use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
   6use editor::Editor;
   7use feature_flags::FeatureFlagAppExt;
   8use feature_flags::FeatureFlagViewExt;
   9use gpui::AsyncWindowContext;
  10use gpui::Subscription;
  11use gpui::Task;
  12use gpui::WeakView;
  13use gpui::{
  14    percentage, Animation, AnimationExt, AnyElement, AppContext, DismissEvent, EventEmitter,
  15    FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View, ViewContext,
  16};
  17use markdown::Markdown;
  18use markdown::MarkdownStyle;
  19use rpc::proto::RegenerateDevServerTokenResponse;
  20use rpc::{
  21    proto::{CreateDevServerResponse, DevServerStatus},
  22    ErrorCode, ErrorExt,
  23};
  24use task::RevealStrategy;
  25use task::SpawnInTerminal;
  26use task::TerminalWorkDir;
  27use terminal_view::terminal_panel::TerminalPanel;
  28use ui::ElevationIndex;
  29use ui::Section;
  30use ui::{
  31    prelude::*, Indicator, List, ListHeader, ListItem, Modal, ModalFooter, ModalHeader,
  32    RadioWithLabel, Tooltip,
  33};
  34use ui_text_field::{FieldLabelLayout, TextField};
  35use util::ResultExt;
  36use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB};
  37
  38use crate::OpenRemote;
  39
  40pub struct DevServerProjects {
  41    mode: Mode,
  42    focus_handle: FocusHandle,
  43    scroll_handle: ScrollHandle,
  44    dev_server_store: Model<dev_server_projects::Store>,
  45    workspace: WeakView<Workspace>,
  46    project_path_input: View<Editor>,
  47    dev_server_name_input: View<TextField>,
  48    markdown: View<Markdown>,
  49    _dev_server_subscription: Subscription,
  50}
  51
  52#[derive(Default)]
  53struct CreateDevServer {
  54    creating: Option<Task<()>>,
  55    dev_server_id: Option<DevServerId>,
  56    access_token: Option<String>,
  57    manual_setup: bool,
  58}
  59
  60struct CreateDevServerProject {
  61    dev_server_id: DevServerId,
  62    creating: bool,
  63    _opening: Option<Subscription>,
  64}
  65
  66enum Mode {
  67    Default(Option<CreateDevServerProject>),
  68    CreateDevServer(CreateDevServer),
  69}
  70
  71impl DevServerProjects {
  72    pub fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
  73        cx.observe_flag::<feature_flags::Remoting, _>(|enabled, workspace, _| {
  74            if enabled {
  75                Self::register_open_remote_action(workspace);
  76            }
  77        })
  78        .detach();
  79
  80        if cx.has_flag::<feature_flags::Remoting>() {
  81            Self::register_open_remote_action(workspace);
  82        }
  83    }
  84
  85    fn register_open_remote_action(workspace: &mut Workspace) {
  86        workspace.register_action(|workspace, _: &OpenRemote, cx| {
  87            let handle = cx.view().downgrade();
  88            workspace.toggle_modal(cx, |cx| Self::new(cx, handle))
  89        });
  90    }
  91
  92    pub fn open(workspace: View<Workspace>, cx: &mut WindowContext) {
  93        workspace.update(cx, |workspace, cx| {
  94            let handle = cx.view().downgrade();
  95            workspace.toggle_modal(cx, |cx| Self::new(cx, handle))
  96        })
  97    }
  98
  99    pub fn new(cx: &mut ViewContext<Self>, workspace: WeakView<Workspace>) -> Self {
 100        let project_path_input = cx.new_view(|cx| {
 101            let mut editor = Editor::single_line(cx);
 102            editor.set_placeholder_text("Project path (~/work/zed, /workspace/zed, …)", cx);
 103            editor
 104        });
 105        let dev_server_name_input = cx.new_view(|cx| {
 106            TextField::new(cx, "Name", "192.168.0.1").with_label(FieldLabelLayout::Hidden)
 107        });
 108
 109        let focus_handle = cx.focus_handle();
 110        let dev_server_store = dev_server_projects::Store::global(cx);
 111
 112        let subscription = cx.observe(&dev_server_store, |_, _, cx| {
 113            cx.notify();
 114        });
 115
 116        let markdown_style = MarkdownStyle {
 117            code_block: gpui::TextStyleRefinement {
 118                font_family: Some("Zed Mono".into()),
 119                color: Some(cx.theme().colors().editor_foreground),
 120                background_color: Some(cx.theme().colors().editor_background),
 121                ..Default::default()
 122            },
 123            inline_code: Default::default(),
 124            block_quote: Default::default(),
 125            link: gpui::TextStyleRefinement {
 126                color: Some(Color::Accent.color(cx)),
 127                ..Default::default()
 128            },
 129            rule_color: Default::default(),
 130            block_quote_border_color: Default::default(),
 131            syntax: cx.theme().syntax().clone(),
 132            selection_background_color: cx.theme().players().local().selection,
 133        };
 134        let markdown = cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx));
 135
 136        Self {
 137            mode: Mode::Default(None),
 138            focus_handle,
 139            scroll_handle: ScrollHandle::new(),
 140            dev_server_store,
 141            project_path_input,
 142            dev_server_name_input,
 143            markdown,
 144            workspace,
 145            _dev_server_subscription: subscription,
 146        }
 147    }
 148
 149    pub fn create_dev_server_project(
 150        &mut self,
 151        dev_server_id: DevServerId,
 152        cx: &mut ViewContext<Self>,
 153    ) {
 154        let mut path = self.project_path_input.read(cx).text(cx).trim().to_string();
 155
 156        if path == "" {
 157            return;
 158        }
 159
 160        if !path.starts_with('/') && !path.starts_with('~') {
 161            path = format!("~/{}", path);
 162        }
 163
 164        if self
 165            .dev_server_store
 166            .read(cx)
 167            .projects_for_server(dev_server_id)
 168            .iter()
 169            .any(|p| p.path == path)
 170        {
 171            cx.spawn(|_, mut cx| async move {
 172                cx.prompt(
 173                    gpui::PromptLevel::Critical,
 174                    "Failed to create project",
 175                    Some(&format!(
 176                        "Project {} already exists for this dev server.",
 177                        path
 178                    )),
 179                    &["Ok"],
 180                )
 181                .await
 182            })
 183            .detach_and_log_err(cx);
 184            return;
 185        }
 186
 187        let create = {
 188            let path = path.clone();
 189            self.dev_server_store.update(cx, |store, cx| {
 190                store.create_dev_server_project(dev_server_id, path, cx)
 191            })
 192        };
 193
 194        cx.spawn(|this, mut cx| async move {
 195            let result = create.await;
 196            this.update(&mut cx, |this, cx| {
 197                if let Ok(result) = &result {
 198                    if let Some(dev_server_project_id) =
 199                        result.dev_server_project.as_ref().map(|p| p.id)
 200                    {
 201                        let subscription =
 202                            cx.observe(&this.dev_server_store, move |this, store, cx| {
 203                                if let Some(project_id) = store
 204                                    .read(cx)
 205                                    .dev_server_project(DevServerProjectId(dev_server_project_id))
 206                                    .and_then(|p| p.project_id)
 207                                {
 208                                    this.project_path_input.update(cx, |editor, cx| {
 209                                        editor.set_text("", cx);
 210                                    });
 211                                    this.mode = Mode::Default(None);
 212                                    if let Some(app_state) = AppState::global(cx).upgrade() {
 213                                        workspace::join_dev_server_project(
 214                                            project_id, app_state, None, cx,
 215                                        )
 216                                        .detach_and_prompt_err(
 217                                            "Could not join project",
 218                                            cx,
 219                                            |_, _| None,
 220                                        )
 221                                    }
 222                                }
 223                            });
 224
 225                        this.mode = Mode::Default(Some(CreateDevServerProject {
 226                            dev_server_id,
 227                            creating: true,
 228                            _opening: Some(subscription),
 229                        }));
 230                    }
 231                } else {
 232                    this.mode = Mode::Default(Some(CreateDevServerProject {
 233                        dev_server_id,
 234                        creating: false,
 235                        _opening: None,
 236                    }));
 237                }
 238            })
 239            .log_err();
 240            result
 241        })
 242        .detach_and_prompt_err("Failed to create project", cx, move |e, _| {
 243            match e.error_code() {
 244                ErrorCode::DevServerOffline => Some(
 245                    "The dev server is offline. Please log in and check it is connected."
 246                        .to_string(),
 247                ),
 248                ErrorCode::DevServerProjectPathDoesNotExist => {
 249                    Some(format!("The path `{}` does not exist on the server.", path))
 250                }
 251                _ => None,
 252            }
 253        });
 254
 255        self.mode = Mode::Default(Some(CreateDevServerProject {
 256            dev_server_id,
 257            creating: true,
 258            _opening: None,
 259        }));
 260    }
 261
 262    pub fn create_or_update_dev_server(
 263        &mut self,
 264        manual_setup: bool,
 265        existing_id: Option<DevServerId>,
 266        access_token: Option<String>,
 267        cx: &mut ViewContext<Self>,
 268    ) {
 269        let name = get_text(&self.dev_server_name_input, cx);
 270        if name.is_empty() {
 271            return;
 272        }
 273
 274        let ssh_connection_string = if manual_setup {
 275            None
 276        } else if name.contains(' ') {
 277            Some(name.clone())
 278        } else {
 279            Some(format!("ssh {}", name))
 280        };
 281
 282        let dev_server = self.dev_server_store.update(cx, {
 283            let access_token = access_token.clone();
 284            |store, cx| {
 285                let ssh_connection_string = ssh_connection_string.clone();
 286                if let Some(dev_server_id) = existing_id {
 287                    let rename = store.rename_dev_server(
 288                        dev_server_id,
 289                        name.clone(),
 290                        ssh_connection_string,
 291                        cx,
 292                    );
 293                    let token = if let Some(access_token) = access_token {
 294                        Task::ready(Ok(RegenerateDevServerTokenResponse {
 295                            dev_server_id: dev_server_id.0,
 296                            access_token,
 297                        }))
 298                    } else {
 299                        store.regenerate_dev_server_token(dev_server_id, cx)
 300                    };
 301                    cx.spawn(|_, _| async move {
 302                        rename.await?;
 303                        let response = token.await?;
 304                        Ok(CreateDevServerResponse {
 305                            dev_server_id: dev_server_id.0,
 306                            name,
 307                            access_token: response.access_token,
 308                        })
 309                    })
 310                } else {
 311                    store.create_dev_server(name, ssh_connection_string.clone(), cx)
 312                }
 313            }
 314        });
 315
 316        let workspace = self.workspace.clone();
 317        let store = dev_server_projects::Store::global(cx);
 318
 319        let task = cx
 320            .spawn({
 321                |this, mut cx| async move {
 322                    let result = dev_server.await;
 323
 324                    match result {
 325                        Ok(dev_server) => {
 326                            if let Some(ssh_connection_string) = ssh_connection_string {
 327                                this.update(&mut cx, |this, cx| {
 328                                    if let Mode::CreateDevServer(CreateDevServer {
 329                                        access_token,
 330                                        dev_server_id,
 331                                        ..
 332                                    }) = &mut this.mode
 333                                    {
 334                                        access_token.replace(dev_server.access_token.clone());
 335                                        dev_server_id
 336                                            .replace(DevServerId(dev_server.dev_server_id));
 337                                    }
 338                                    cx.notify();
 339                                })?;
 340
 341                                spawn_ssh_task(
 342                                    workspace
 343                                        .upgrade()
 344                                        .ok_or_else(|| anyhow!("workspace dropped"))?,
 345                                    store,
 346                                    DevServerId(dev_server.dev_server_id),
 347                                    ssh_connection_string,
 348                                    dev_server.access_token.clone(),
 349                                    &mut cx,
 350                                )
 351                                .await
 352                                .log_err();
 353                            }
 354
 355                            this.update(&mut cx, |this, cx| {
 356                                this.focus_handle.focus(cx);
 357                                this.mode = Mode::CreateDevServer(CreateDevServer {
 358                                    creating: None,
 359                                    dev_server_id: Some(DevServerId(dev_server.dev_server_id)),
 360                                    access_token: Some(dev_server.access_token),
 361                                    manual_setup,
 362                                });
 363                                cx.notify();
 364                            })?;
 365                            Ok(())
 366                        }
 367                        Err(e) => {
 368                            this.update(&mut cx, |this, cx| {
 369                                this.mode = Mode::CreateDevServer(CreateDevServer {
 370                                    creating: None,
 371                                    dev_server_id: existing_id,
 372                                    access_token: None,
 373                                    manual_setup,
 374                                });
 375                                cx.notify()
 376                            })
 377                            .log_err();
 378
 379                            return Err(e);
 380                        }
 381                    }
 382                }
 383            })
 384            .prompt_err("Failed to create server", cx, |_, _| None);
 385
 386        self.mode = Mode::CreateDevServer(CreateDevServer {
 387            creating: Some(task),
 388            dev_server_id: existing_id,
 389            access_token,
 390            manual_setup,
 391        });
 392        cx.notify()
 393    }
 394
 395    fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) {
 396        let store = self.dev_server_store.read(cx);
 397        let prompt = if store.projects_for_server(id).is_empty()
 398            && store
 399                .dev_server(id)
 400                .is_some_and(|server| server.status == DevServerStatus::Offline)
 401        {
 402            None
 403        } else {
 404            Some(cx.prompt(
 405                gpui::PromptLevel::Warning,
 406                "Are you sure?",
 407                Some("This will delete the dev server and all of its remote projects."),
 408                &["Delete", "Cancel"],
 409            ))
 410        };
 411
 412        cx.spawn(|this, mut cx| async move {
 413            if let Some(prompt) = prompt {
 414                if prompt.await? != 0 {
 415                    return Ok(());
 416                }
 417            }
 418
 419            let project_ids: Vec<DevServerProjectId> = this.update(&mut cx, |this, cx| {
 420                this.dev_server_store.update(cx, |store, _| {
 421                    store
 422                        .projects_for_server(id)
 423                        .into_iter()
 424                        .map(|project| project.id)
 425                        .collect()
 426                })
 427            })?;
 428
 429            this.update(&mut cx, |this, cx| {
 430                this.dev_server_store
 431                    .update(cx, |store, cx| store.delete_dev_server(id, cx))
 432            })?
 433            .await?;
 434
 435            for id in project_ids {
 436                WORKSPACE_DB
 437                    .delete_workspace_by_dev_server_project_id(id)
 438                    .await
 439                    .log_err();
 440            }
 441            Ok(())
 442        })
 443        .detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None);
 444    }
 445
 446    fn delete_dev_server_project(
 447        &mut self,
 448        id: DevServerProjectId,
 449        path: &str,
 450        cx: &mut ViewContext<Self>,
 451    ) {
 452        let answer = cx.prompt(
 453            gpui::PromptLevel::Warning,
 454            format!("Delete \"{}\"?", path).as_str(),
 455            Some("This will delete the remote project. You can always re-add it later."),
 456            &["Delete", "Cancel"],
 457        );
 458
 459        cx.spawn(|this, mut cx| async move {
 460            let answer = answer.await?;
 461
 462            if answer != 0 {
 463                return Ok(());
 464            }
 465
 466            this.update(&mut cx, |this, cx| {
 467                this.dev_server_store
 468                    .update(cx, |store, cx| store.delete_dev_server_project(id, cx))
 469            })?
 470            .await?;
 471
 472            WORKSPACE_DB
 473                .delete_workspace_by_dev_server_project_id(id)
 474                .await
 475                .log_err();
 476
 477            Ok(())
 478        })
 479        .detach_and_prompt_err("Failed to delete dev server project", cx, |_, _| None);
 480    }
 481
 482    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
 483        match &self.mode {
 484            Mode::Default(None) => {}
 485            Mode::Default(Some(create_project)) => {
 486                self.create_dev_server_project(create_project.dev_server_id, cx);
 487            }
 488            Mode::CreateDevServer(state) => {
 489                if state.creating.is_none() || state.dev_server_id.is_some() {
 490                    self.create_or_update_dev_server(
 491                        state.manual_setup,
 492                        state.dev_server_id,
 493                        state.access_token.clone(),
 494                        cx,
 495                    );
 496                }
 497            }
 498        }
 499    }
 500
 501    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
 502        match self.mode {
 503            Mode::Default(None) => cx.emit(DismissEvent),
 504            _ => {
 505                self.mode = Mode::Default(None);
 506                self.focus_handle(cx).focus(cx);
 507                cx.notify();
 508            }
 509        }
 510    }
 511
 512    fn render_dev_server(
 513        &mut self,
 514        dev_server: &DevServer,
 515        create_project: Option<bool>,
 516        cx: &mut ViewContext<Self>,
 517    ) -> impl IntoElement {
 518        let dev_server_id = dev_server.id;
 519        let status = dev_server.status;
 520        let dev_server_name = dev_server.name.clone();
 521        let manual_setup = dev_server.ssh_connection_string.is_none();
 522
 523        v_flex()
 524            .w_full()
 525            .child(
 526                h_flex().group("dev-server").justify_between().child(
 527                    h_flex()
 528                        .gap_2()
 529                        .child(
 530                            div()
 531                                .id(("status", dev_server.id.0))
 532                                .relative()
 533                                .child(Icon::new(IconName::Server).size(IconSize::Small))
 534                                .child(div().absolute().bottom_0().left(rems_from_px(8.0)).child(
 535                                    Indicator::dot().color(match status {
 536                                        DevServerStatus::Online => Color::Created,
 537                                        DevServerStatus::Offline => Color::Hidden,
 538                                    }),
 539                                ))
 540                                .tooltip(move |cx| {
 541                                    Tooltip::text(
 542                                        match status {
 543                                            DevServerStatus::Online => "Online",
 544                                            DevServerStatus::Offline => "Offline",
 545                                        },
 546                                        cx,
 547                                    )
 548                                }),
 549                        )
 550                        .child(
 551                            div()
 552                                .max_w(rems(26.))
 553                                .overflow_hidden()
 554                                .whitespace_nowrap()
 555                                .child(Label::new(dev_server_name.clone())),
 556                        )
 557                        .child(
 558                            h_flex()
 559                                .visible_on_hover("dev-server")
 560                                .gap_1()
 561                                .child(
 562                                    IconButton::new("edit-dev-server", IconName::Pencil)
 563                                        .on_click(cx.listener(move |this, _, cx| {
 564                                            this.mode = Mode::CreateDevServer(CreateDevServer {
 565                                                dev_server_id: Some(dev_server_id),
 566                                                creating: None,
 567                                                access_token: None,
 568                                                manual_setup,
 569                                            });
 570                                            let dev_server_name = dev_server_name.clone();
 571                                            this.dev_server_name_input.update(
 572                                                cx,
 573                                                move |input, cx| {
 574                                                    input.editor().update(cx, move |editor, cx| {
 575                                                        editor.set_text(dev_server_name, cx)
 576                                                    })
 577                                                },
 578                                            )
 579                                        }))
 580                                        .tooltip(|cx| Tooltip::text("Edit dev server", cx)),
 581                                )
 582                                .child({
 583                                    let dev_server_id = dev_server.id;
 584                                    IconButton::new("remove-dev-server", IconName::Trash)
 585                                        .on_click(cx.listener(move |this, _, cx| {
 586                                            this.delete_dev_server(dev_server_id, cx)
 587                                        }))
 588                                        .tooltip(|cx| Tooltip::text("Remove dev server", cx))
 589                                }),
 590                        ),
 591                ),
 592            )
 593            .child(
 594                v_flex()
 595                    .w_full()
 596                    .bg(cx.theme().colors().background)
 597                    .border_1()
 598                    .border_color(cx.theme().colors().border_variant)
 599                    .rounded_md()
 600                    .my_1()
 601                    .py_0p5()
 602                    .px_3()
 603                    .child(
 604                        List::new()
 605                            .empty_message("No projects.")
 606                            .children(
 607                                self.dev_server_store
 608                                    .read(cx)
 609                                    .projects_for_server(dev_server.id)
 610                                    .iter()
 611                                    .map(|p| self.render_dev_server_project(p, cx)),
 612                            )
 613                            .when(
 614                                create_project.is_none()
 615                                    && dev_server.status == DevServerStatus::Online,
 616                                |el| {
 617                                    el.child(
 618                                        ListItem::new("new-remote_project")
 619                                            .start_slot(Icon::new(IconName::Plus))
 620                                            .child(Label::new("Open folder…"))
 621                                            .on_click(cx.listener(move |this, _, cx| {
 622                                                this.mode =
 623                                                    Mode::Default(Some(CreateDevServerProject {
 624                                                        dev_server_id,
 625                                                        creating: false,
 626                                                        _opening: None,
 627                                                    }));
 628                                                this.project_path_input
 629                                                    .read(cx)
 630                                                    .focus_handle(cx)
 631                                                    .focus(cx);
 632                                                cx.notify();
 633                                            })),
 634                                    )
 635                                },
 636                            )
 637                            .when_some(create_project, |el, creating| {
 638                                el.child(self.render_create_new_project(creating, cx))
 639                            }),
 640                    ),
 641            )
 642    }
 643
 644    fn render_create_new_project(
 645        &mut self,
 646        creating: bool,
 647        _: &mut ViewContext<Self>,
 648    ) -> impl IntoElement {
 649        ListItem::new("create-remote-project")
 650            .disabled(true)
 651            .start_slot(Icon::new(IconName::FileTree).color(Color::Muted))
 652            .child(self.project_path_input.clone())
 653            .child(div().w(IconSize::Medium.rems()).when(creating, |el| {
 654                el.child(
 655                    Icon::new(IconName::ArrowCircle)
 656                        .size(IconSize::Medium)
 657                        .with_animation(
 658                            "arrow-circle",
 659                            Animation::new(Duration::from_secs(2)).repeat(),
 660                            |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
 661                        ),
 662                )
 663            }))
 664    }
 665
 666    fn render_dev_server_project(
 667        &mut self,
 668        project: &DevServerProject,
 669        cx: &mut ViewContext<Self>,
 670    ) -> impl IntoElement {
 671        let dev_server_project_id = project.id;
 672        let project_id = project.project_id;
 673        let is_online = project_id.is_some();
 674        let project_path = project.path.clone();
 675
 676        ListItem::new(("remote-project", dev_server_project_id.0))
 677            .start_slot(Icon::new(IconName::FileTree).when(!is_online, |icon| icon.color(Color::Muted)))
 678            .child(
 679                    Label::new(project.path.clone())
 680            )
 681            .on_click(cx.listener(move |_, _, cx| {
 682                if let Some(project_id) = project_id {
 683                    if let Some(app_state) = AppState::global(cx).upgrade() {
 684                        workspace::join_dev_server_project(project_id, app_state, None, cx)
 685                            .detach_and_prompt_err("Could not join project", cx, |_, _| None)
 686                    }
 687                } else {
 688                    cx.spawn(|_, mut cx| async move {
 689                        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();
 690                    }).detach();
 691                }
 692            }))
 693            .end_hover_slot::<AnyElement>(Some(IconButton::new("remove-remote-project", IconName::Trash)
 694                .on_click(cx.listener(move |this, _, cx| {
 695                    this.delete_dev_server_project(dev_server_project_id, &project_path, cx)
 696                }))
 697                .tooltip(|cx| Tooltip::text("Delete remote project", cx)).into_any_element()))
 698    }
 699
 700    fn render_create_dev_server(
 701        &self,
 702        state: &CreateDevServer,
 703        cx: &mut ViewContext<Self>,
 704    ) -> impl IntoElement {
 705        let creating = state.creating.is_some();
 706        let dev_server_id = state.dev_server_id;
 707        let access_token = state.access_token.clone();
 708        let manual_setup = state.manual_setup;
 709
 710        let status = dev_server_id
 711            .map(|id| self.dev_server_store.read(cx).dev_server_status(id))
 712            .unwrap_or_default();
 713
 714        let name = self.dev_server_name_input.update(cx, |input, cx| {
 715            input.editor().update(cx, |editor, cx| {
 716                if editor.text(cx).is_empty() {
 717                    if manual_setup {
 718                        editor.set_placeholder_text("example-server", cx)
 719                    } else {
 720                        editor.set_placeholder_text("ssh host", cx)
 721                    }
 722                }
 723                editor.text(cx)
 724            })
 725        });
 726
 727        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.";
 728        const SSH_SETUP_MESSAGE: &str = "Enter the command you use to ssh into this server.\nFor example: `ssh me@my.server` or `gh cs ssh -c example`.";
 729
 730        Modal::new("create-dev-server", Some(self.scroll_handle.clone()))
 731            .header(
 732                ModalHeader::new()
 733                    .headline("Create Dev Server")
 734                    .show_back_button(true),
 735            )
 736            .section(
 737                Section::new()
 738                    .header(if manual_setup {
 739                        "Server Name".into()
 740                    } else {
 741                        "SSH arguments".into()
 742                    })
 743                    .child(
 744                        div()
 745                            .max_w(rems(16.))
 746                            .child(self.dev_server_name_input.clone()),
 747                    ),
 748            )
 749            .section(
 750                Section::new_contained()
 751                    .header("Connection Method".into())
 752                    .child(
 753                        v_flex()
 754                            .w_full()
 755                            .gap_y(Spacing::Large.rems(cx))
 756                            .child(
 757                                v_flex()
 758                                    .child(RadioWithLabel::new(
 759                                        "use-server-name-in-ssh",
 760                                        Label::new("Connect via SSH (default)"),
 761                                        !manual_setup,
 762                                        cx.listener({
 763                                            move |this, _, cx| {
 764                                                if let Mode::CreateDevServer(CreateDevServer {
 765                                                    manual_setup,
 766                                                    ..
 767                                                }) = &mut this.mode
 768                                                {
 769                                                    *manual_setup = false;
 770                                                }
 771                                                cx.notify()
 772                                            }
 773                                        }),
 774                                    ))
 775                                    .child(RadioWithLabel::new(
 776                                        "use-server-name-in-ssh",
 777                                        Label::new("Manual Setup"),
 778                                        manual_setup,
 779                                        cx.listener({
 780                                            move |this, _, cx| {
 781                                                if let Mode::CreateDevServer(CreateDevServer {
 782                                                    manual_setup,
 783                                                    ..
 784                                                }) = &mut this.mode
 785                                                {
 786                                                    *manual_setup = true;
 787                                                }
 788                                                cx.notify()
 789                                            }
 790                                        }),
 791                                    )),
 792                            )
 793                            .when(dev_server_id.is_none(), |el| {
 794                                el.child(
 795                                    if manual_setup {
 796                                        Label::new(MANUAL_SETUP_MESSAGE)
 797                                    } else {
 798                                        Label::new(SSH_SETUP_MESSAGE)
 799                                    }
 800                                    .size(LabelSize::Small)
 801                                    .color(Color::Muted),
 802                                )
 803                            })
 804                            .when(dev_server_id.is_some() && access_token.is_none(), |el| {
 805                                el.child(
 806                                    if manual_setup {
 807                                        Label::new(
 808                                            "Note: updating the dev server generate a new token",
 809                                        )
 810                                    } else {
 811                                        Label::new(
 812                                            "Enter the command you use to ssh into this server.\n\
 813                                        For example: `ssh me@my.server` or `gh cs ssh -c example`.",
 814                                        )
 815                                    }
 816                                    .size(LabelSize::Small)
 817                                    .color(Color::Muted),
 818                                )
 819                            })
 820                            .when_some(access_token.clone(), {
 821                                |el, access_token| {
 822                                    el.child(self.render_dev_server_token_creating(
 823                                        access_token,
 824                                        name,
 825                                        manual_setup,
 826                                        status,
 827                                        creating,
 828                                        cx,
 829                                    ))
 830                                }
 831                            }),
 832                    ),
 833            )
 834            .footer(
 835                ModalFooter::new().end_slot(if status == DevServerStatus::Online {
 836                    Button::new("create-dev-server", "Done")
 837                        .style(ButtonStyle::Filled)
 838                        .layer(ElevationIndex::ModalSurface)
 839                        .on_click(cx.listener(move |this, _, cx| {
 840                            cx.focus(&this.focus_handle);
 841                            this.mode = Mode::Default(None);
 842                            cx.notify();
 843                        }))
 844                } else {
 845                    Button::new(
 846                        "create-dev-server",
 847                        if manual_setup {
 848                            if dev_server_id.is_some() {
 849                                "Update"
 850                            } else {
 851                                "Create"
 852                            }
 853                        } else {
 854                            if dev_server_id.is_some() {
 855                                "Reconnect"
 856                            } else {
 857                                "Connect"
 858                            }
 859                        },
 860                    )
 861                    .style(ButtonStyle::Filled)
 862                    .layer(ElevationIndex::ModalSurface)
 863                    .disabled(creating && dev_server_id.is_none())
 864                    .on_click(cx.listener({
 865                        let access_token = access_token.clone();
 866                        move |this, _, cx| {
 867                            this.create_or_update_dev_server(
 868                                manual_setup,
 869                                dev_server_id,
 870                                access_token.clone(),
 871                                cx,
 872                            );
 873                        }
 874                    }))
 875                }),
 876            )
 877    }
 878
 879    fn render_dev_server_token_creating(
 880        &self,
 881        access_token: String,
 882        dev_server_name: String,
 883        manual_setup: bool,
 884        status: DevServerStatus,
 885        creating: bool,
 886        cx: &mut ViewContext<Self>,
 887    ) -> Div {
 888        self.markdown.update(cx, |markdown, cx| {
 889            if manual_setup {
 890                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);
 891            } else {
 892                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);
 893            }
 894        });
 895
 896        v_flex()
 897            .pl_2()
 898            .pt_2()
 899            .gap_2()
 900            .child(v_flex().w_full().text_sm().child(self.markdown.clone()))
 901            .map(|el| {
 902                if status == DevServerStatus::Offline && !manual_setup && !creating {
 903                    el.child(
 904                        h_flex()
 905                            .gap_2()
 906                            .child(Icon::new(IconName::Disconnected).size(IconSize::Medium))
 907                            .child(Label::new("Not connected")),
 908                    )
 909                } else if status == DevServerStatus::Offline {
 910                    el.child(Self::render_loading_spinner("Waiting for connection…"))
 911                } else {
 912                    el.child(Label::new("🎊 Connection established!"))
 913                }
 914            })
 915    }
 916
 917    fn render_loading_spinner(label: impl Into<SharedString>) -> Div {
 918        h_flex()
 919            .gap_2()
 920            .child(
 921                Icon::new(IconName::ArrowCircle)
 922                    .size(IconSize::Medium)
 923                    .with_animation(
 924                        "arrow-circle",
 925                        Animation::new(Duration::from_secs(2)).repeat(),
 926                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
 927                    ),
 928            )
 929            .child(Label::new(label))
 930    }
 931
 932    fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 933        let dev_servers = self.dev_server_store.read(cx).dev_servers();
 934
 935        let Mode::Default(create_dev_server_project) = &self.mode else {
 936            unreachable!()
 937        };
 938
 939        let mut is_creating = None;
 940        let mut creating_dev_server = None;
 941        if let Some(CreateDevServerProject {
 942            creating,
 943            dev_server_id,
 944            ..
 945        }) = create_dev_server_project
 946        {
 947            is_creating = Some(*creating);
 948            creating_dev_server = Some(*dev_server_id);
 949        };
 950
 951        Modal::new("remote-projects", Some(self.scroll_handle.clone()))
 952            .header(
 953                ModalHeader::new()
 954                    .show_dismiss_button(true)
 955                    .child(Headline::new("Remote Projects").size(HeadlineSize::Small)),
 956            )
 957            .section(
 958                Section::new().child(
 959                    div().mb_4().child(
 960                        List::new()
 961                            .empty_message("No dev servers registered.")
 962                            .header(Some(
 963                                ListHeader::new("Dev Servers").end_slot(
 964                                    Button::new("register-dev-server-button", "New Server")
 965                                        .icon(IconName::Plus)
 966                                        .icon_position(IconPosition::Start)
 967                                        .tooltip(|cx| {
 968                                            Tooltip::text("Register a new dev server", cx)
 969                                        })
 970                                        .on_click(cx.listener(|this, _, cx| {
 971                                            this.mode =
 972                                                Mode::CreateDevServer(CreateDevServer::default());
 973                                            this.dev_server_name_input.update(
 974                                                cx,
 975                                                |text_field, cx| {
 976                                                    text_field.editor().update(cx, |editor, cx| {
 977                                                        editor.set_text("", cx);
 978                                                    });
 979                                                },
 980                                            );
 981                                            cx.notify();
 982                                        })),
 983                                ),
 984                            ))
 985                            .children(dev_servers.iter().map(|dev_server| {
 986                                let creating = if creating_dev_server == Some(dev_server.id) {
 987                                    is_creating
 988                                } else {
 989                                    None
 990                                };
 991                                self.render_dev_server(dev_server, creating, cx)
 992                                    .into_any_element()
 993                            })),
 994                    ),
 995                ),
 996            )
 997    }
 998}
 999
1000fn get_text(element: &View<TextField>, cx: &mut WindowContext) -> String {
1001    element
1002        .read(cx)
1003        .editor()
1004        .read(cx)
1005        .text(cx)
1006        .trim()
1007        .to_string()
1008}
1009
1010impl ModalView for DevServerProjects {}
1011
1012impl FocusableView for DevServerProjects {
1013    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
1014        self.focus_handle.clone()
1015    }
1016}
1017
1018impl EventEmitter<DismissEvent> for DevServerProjects {}
1019
1020impl Render for DevServerProjects {
1021    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1022        div()
1023            .track_focus(&self.focus_handle)
1024            .elevation_3(cx)
1025            .key_context("DevServerModal")
1026            .on_action(cx.listener(Self::cancel))
1027            .on_action(cx.listener(Self::confirm))
1028            .capture_any_mouse_down(cx.listener(|this, _, cx| {
1029                this.focus_handle(cx).focus(cx);
1030            }))
1031            .on_mouse_down_out(cx.listener(|this, _, cx| {
1032                if matches!(this.mode, Mode::Default(None)) {
1033                    cx.emit(DismissEvent)
1034                }
1035            }))
1036            .w(rems(34.))
1037            .max_h(rems(40.))
1038            .child(match &self.mode {
1039                Mode::Default(_) => self.render_default(cx).into_any_element(),
1040                Mode::CreateDevServer(state) => {
1041                    self.render_create_dev_server(state, cx).into_any_element()
1042                }
1043            })
1044    }
1045}
1046
1047pub fn reconnect_to_dev_server(
1048    workspace: View<Workspace>,
1049    dev_server: DevServer,
1050    cx: &mut WindowContext,
1051) -> Task<anyhow::Result<()>> {
1052    let Some(ssh_connection_string) = dev_server.ssh_connection_string else {
1053        return Task::ready(Err(anyhow!("can't reconnect, no ssh_connection_string")));
1054    };
1055    let dev_server_store = dev_server_projects::Store::global(cx);
1056    let get_access_token = dev_server_store.update(cx, |store, cx| {
1057        store.regenerate_dev_server_token(dev_server.id, cx)
1058    });
1059
1060    cx.spawn(|mut cx| async move {
1061        let access_token = get_access_token.await?.access_token;
1062
1063        spawn_ssh_task(
1064            workspace,
1065            dev_server_store,
1066            dev_server.id,
1067            ssh_connection_string.to_string(),
1068            access_token,
1069            &mut cx,
1070        )
1071        .await
1072    })
1073}
1074
1075pub async fn spawn_ssh_task(
1076    workspace: View<Workspace>,
1077    dev_server_store: Model<dev_server_projects::Store>,
1078    dev_server_id: DevServerId,
1079    ssh_connection_string: String,
1080    access_token: String,
1081    cx: &mut AsyncWindowContext,
1082) -> anyhow::Result<()> {
1083    let terminal_panel = workspace
1084        .update(cx, |workspace, cx| workspace.panel::<TerminalPanel>(cx))
1085        .ok()
1086        .flatten()
1087        .with_context(|| anyhow!("No terminal panel"))?;
1088
1089    let command = "sh".to_string();
1090    let args = vec![
1091        "-x".to_string(),
1092        "-c".to_string(),
1093        format!(
1094            r#"~/.local/bin/zed -v >/dev/stderr || (curl -sSL https://zed.dev/install.sh || wget -qO- https://zed.dev/install.sh) | bash && ~/.local/bin/zed --dev-server-token {}"#,
1095            access_token
1096        ),
1097    ];
1098
1099    let ssh_connection_string = ssh_connection_string.to_string();
1100
1101    let terminal = terminal_panel
1102        .update(cx, |terminal_panel, cx| {
1103            terminal_panel.spawn_in_new_terminal(
1104                SpawnInTerminal {
1105                    id: task::TaskId("ssh-remote".into()),
1106                    full_label: "Install zed over ssh".into(),
1107                    label: "Install zed over ssh".into(),
1108                    command,
1109                    args,
1110                    command_label: ssh_connection_string.clone(),
1111                    cwd: Some(TerminalWorkDir::Ssh {
1112                        ssh_command: ssh_connection_string,
1113                        path: None,
1114                    }),
1115                    env: Default::default(),
1116                    use_new_terminal: true,
1117                    allow_concurrent_runs: false,
1118                    reveal: RevealStrategy::Always,
1119                },
1120                cx,
1121            )
1122        })?
1123        .await?;
1124
1125    terminal
1126        .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
1127        .await;
1128
1129    // 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.
1130    if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
1131        == DevServerStatus::Offline
1132    {
1133        cx.background_executor()
1134            .timer(Duration::from_millis(200))
1135            .await
1136    }
1137
1138    if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
1139        == DevServerStatus::Offline
1140    {
1141        return Err(anyhow!("couldn't reconnect"))?;
1142    }
1143
1144    Ok(())
1145}