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        Modal::new("create-dev-server", Some(self.scroll_handle.clone()))
 728            .header(
 729                ModalHeader::new()
 730                    .headline("Create Dev Server")
 731                    .show_back_button(true),
 732            )
 733            .section(
 734                Section::new()
 735                    .header(if manual_setup { "Server Name".into()} else { "SSH arguments".into()})
 736                    .child(
 737                        div()
 738                            .max_w(rems(16.))
 739                            .child(self.dev_server_name_input.clone())
 740                    ),
 741            )
 742            .section(
 743                Section::new_contained()
 744                    .header("Connection Method".into())
 745                    .child(
 746                        v_flex()
 747                            .w_full()
 748                            .gap_y(Spacing::Large.rems(cx))
 749                            .child(v_flex().child(RadioWithLabel::new(
 750                                "use-server-name-in-ssh",
 751                                Label::new("Connect via SSH (default)"),
 752                                !manual_setup,
 753                                cx.listener({
 754                                    move |this, _, cx| {
 755                                        if let Mode::CreateDevServer(CreateDevServer{ manual_setup, .. }) = &mut this.mode {
 756                                            *manual_setup = false;
 757                                        }
 758                                        cx.notify()
 759                                    }
 760                                }),
 761                            ))
 762                            .child(RadioWithLabel::new(
 763                                "use-server-name-in-ssh",
 764                                Label::new("Manual Setup"),
 765                                manual_setup,
 766                                cx.listener({
 767                                    move |this, _, cx| {
 768                                        if let Mode::CreateDevServer(CreateDevServer{ manual_setup, .. }) = &mut this.mode {
 769                                            *manual_setup = true;
 770                                        }
 771                                        cx.notify()
 772                                }}),
 773                            )))
 774                            .when(dev_server_id.is_none(), |el| {
 775                                el.child(
 776                                    if manual_setup {
 777                                        Label::new(
 778                                            "Click create to generate a token for this server. The next step will provide instructions for setting zed up on that machine."
 779                                                )
 780                                    } else {
 781                                        Label::new(
 782                                            "Enter the command you use to ssh into this server.\n\
 783                                            For example: `ssh me@my.server` or `gh cs ssh -c example`."
 784                                            )
 785                                }.size(LabelSize::Small).color(Color::Muted))
 786                            })
 787                            .when(dev_server_id.is_some() && access_token.is_none(),|el|{
 788                                el.child(
 789                                if manual_setup {
 790                                    Label::new(
 791                                        "Note: updating the dev server generate a new token"
 792                                            )
 793                                } else {
 794                                    Label::new(
 795                                        "Enter the command you use to ssh into this server.\n\
 796                                        For example: `ssh me@my.server` or `gh cs ssh -c example`."
 797                                        )
 798                                }.size(LabelSize::Small).color(Color::Muted)
 799                                )
 800                            })
 801                            .when_some(access_token.clone(), {
 802                                |el, access_token| {
 803                                el.child(
 804                                    self.render_dev_server_token_creating(access_token, name, manual_setup, status, creating,  cx)
 805                                )
 806                            }}))
 807            )
 808            .footer(ModalFooter::new().end_slot(
 809                if status == DevServerStatus::Online {
 810                    Button::new("create-dev-server", "Done")
 811                        .style(ButtonStyle::Filled)
 812                        .layer(ElevationIndex::ModalSurface)
 813                        .on_click(cx.listener(move |this, _, cx| {
 814                            cx.focus(&this.focus_handle);
 815                            this.mode = Mode::Default(None);
 816                            cx.notify();
 817                        }))
 818                } else {
 819                    Button::new("create-dev-server", if manual_setup { if dev_server_id.is_some() { "Update" } else { "Create"} } else { if dev_server_id.is_some() { "Reconnect" } else { "Connect"} })
 820                        .style(ButtonStyle::Filled)
 821                        .layer(ElevationIndex::ModalSurface)
 822                        .disabled(creating && dev_server_id.is_none())
 823                        .on_click(cx.listener({
 824                            let access_token = access_token.clone();
 825                            move |this, _, cx| {
 826                            this.create_or_update_dev_server(manual_setup, dev_server_id, access_token.clone(), cx);
 827                        }}))
 828                }
 829            ))
 830    }
 831
 832    fn render_dev_server_token_creating(
 833        &self,
 834        access_token: String,
 835        dev_server_name: String,
 836        manual_setup: bool,
 837        status: DevServerStatus,
 838        creating: bool,
 839        cx: &mut ViewContext<Self>,
 840    ) -> Div {
 841        self.markdown.update(cx, |markdown, cx| {
 842            if manual_setup {
 843                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);
 844            } else {
 845                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);
 846            }
 847        });
 848
 849        v_flex()
 850            .pl_2()
 851            .pt_2()
 852            .gap_2()
 853            .child(v_flex().w_full().text_sm().child(self.markdown.clone()))
 854            .map(|el| {
 855                if status == DevServerStatus::Offline && !manual_setup && !creating {
 856                    el.child(
 857                        h_flex()
 858                            .gap_2()
 859                            .child(Icon::new(IconName::Disconnected).size(IconSize::Medium))
 860                            .child(Label::new("Not connected")),
 861                    )
 862                } else if status == DevServerStatus::Offline {
 863                    el.child(Self::render_loading_spinner("Waiting for connection…"))
 864                } else {
 865                    el.child(Label::new("🎊 Connection established!"))
 866                }
 867            })
 868    }
 869
 870    fn render_loading_spinner(label: impl Into<SharedString>) -> Div {
 871        h_flex()
 872            .gap_2()
 873            .child(
 874                Icon::new(IconName::ArrowCircle)
 875                    .size(IconSize::Medium)
 876                    .with_animation(
 877                        "arrow-circle",
 878                        Animation::new(Duration::from_secs(2)).repeat(),
 879                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
 880                    ),
 881            )
 882            .child(Label::new(label))
 883    }
 884
 885    fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 886        let dev_servers = self.dev_server_store.read(cx).dev_servers();
 887
 888        let Mode::Default(create_dev_server_project) = &self.mode else {
 889            unreachable!()
 890        };
 891
 892        let mut is_creating = None;
 893        let mut creating_dev_server = None;
 894        if let Some(CreateDevServerProject {
 895            creating,
 896            dev_server_id,
 897            ..
 898        }) = create_dev_server_project
 899        {
 900            is_creating = Some(*creating);
 901            creating_dev_server = Some(*dev_server_id);
 902        };
 903
 904        Modal::new("remote-projects", Some(self.scroll_handle.clone()))
 905            .header(
 906                ModalHeader::new()
 907                    .show_dismiss_button(true)
 908                    .child(Headline::new("Remote Projects").size(HeadlineSize::Small)),
 909            )
 910            .section(
 911                Section::new().child(
 912                    div().mb_4().child(
 913                        List::new()
 914                            .empty_message("No dev servers registered.")
 915                            .header(Some(
 916                                ListHeader::new("Dev Servers").end_slot(
 917                                    Button::new("register-dev-server-button", "New Server")
 918                                        .icon(IconName::Plus)
 919                                        .icon_position(IconPosition::Start)
 920                                        .tooltip(|cx| {
 921                                            Tooltip::text("Register a new dev server", cx)
 922                                        })
 923                                        .on_click(cx.listener(|this, _, cx| {
 924                                            this.mode =
 925                                                Mode::CreateDevServer(CreateDevServer::default());
 926                                            this.dev_server_name_input.update(
 927                                                cx,
 928                                                |text_field, cx| {
 929                                                    text_field.editor().update(cx, |editor, cx| {
 930                                                        editor.set_text("", cx);
 931                                                    });
 932                                                },
 933                                            );
 934                                            cx.notify();
 935                                        })),
 936                                ),
 937                            ))
 938                            .children(dev_servers.iter().map(|dev_server| {
 939                                let creating = if creating_dev_server == Some(dev_server.id) {
 940                                    is_creating
 941                                } else {
 942                                    None
 943                                };
 944                                self.render_dev_server(dev_server, creating, cx)
 945                                    .into_any_element()
 946                            })),
 947                    ),
 948                ),
 949            )
 950    }
 951}
 952
 953fn get_text(element: &View<TextField>, cx: &mut WindowContext) -> String {
 954    element
 955        .read(cx)
 956        .editor()
 957        .read(cx)
 958        .text(cx)
 959        .trim()
 960        .to_string()
 961}
 962
 963impl ModalView for DevServerProjects {}
 964
 965impl FocusableView for DevServerProjects {
 966    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
 967        self.focus_handle.clone()
 968    }
 969}
 970
 971impl EventEmitter<DismissEvent> for DevServerProjects {}
 972
 973impl Render for DevServerProjects {
 974    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 975        div()
 976            .track_focus(&self.focus_handle)
 977            .elevation_3(cx)
 978            .key_context("DevServerModal")
 979            .on_action(cx.listener(Self::cancel))
 980            .on_action(cx.listener(Self::confirm))
 981            .capture_any_mouse_down(cx.listener(|this, _, cx| {
 982                this.focus_handle(cx).focus(cx);
 983            }))
 984            .on_mouse_down_out(cx.listener(|this, _, cx| {
 985                if matches!(this.mode, Mode::Default(None)) {
 986                    cx.emit(DismissEvent)
 987                }
 988            }))
 989            .w(rems(34.))
 990            .max_h(rems(40.))
 991            .child(match &self.mode {
 992                Mode::Default(_) => self.render_default(cx).into_any_element(),
 993                Mode::CreateDevServer(state) => {
 994                    self.render_create_dev_server(state, cx).into_any_element()
 995                }
 996            })
 997    }
 998}
 999
1000pub fn reconnect_to_dev_server(
1001    workspace: View<Workspace>,
1002    dev_server: DevServer,
1003    cx: &mut WindowContext,
1004) -> Task<anyhow::Result<()>> {
1005    let Some(ssh_connection_string) = dev_server.ssh_connection_string else {
1006        return Task::ready(Err(anyhow!("can't reconnect, no ssh_connection_string")));
1007    };
1008    let dev_server_store = dev_server_projects::Store::global(cx);
1009    let get_access_token = dev_server_store.update(cx, |store, cx| {
1010        store.regenerate_dev_server_token(dev_server.id, cx)
1011    });
1012
1013    cx.spawn(|mut cx| async move {
1014        let access_token = get_access_token.await?.access_token;
1015
1016        spawn_ssh_task(
1017            workspace,
1018            dev_server_store,
1019            dev_server.id,
1020            ssh_connection_string.to_string(),
1021            access_token,
1022            &mut cx,
1023        )
1024        .await
1025    })
1026}
1027
1028pub async fn spawn_ssh_task(
1029    workspace: View<Workspace>,
1030    dev_server_store: Model<dev_server_projects::Store>,
1031    dev_server_id: DevServerId,
1032    ssh_connection_string: String,
1033    access_token: String,
1034    cx: &mut AsyncWindowContext,
1035) -> anyhow::Result<()> {
1036    let terminal_panel = workspace
1037        .update(cx, |workspace, cx| workspace.panel::<TerminalPanel>(cx))
1038        .ok()
1039        .flatten()
1040        .with_context(|| anyhow!("No terminal panel"))?;
1041
1042    let command = "sh".to_string();
1043    let args = vec![
1044        "-x".to_string(),
1045        "-c".to_string(),
1046        format!(
1047            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 {}"#,
1048            access_token
1049        ),
1050    ];
1051
1052    let ssh_connection_string = ssh_connection_string.to_string();
1053
1054    let terminal = terminal_panel
1055        .update(cx, |terminal_panel, cx| {
1056            terminal_panel.spawn_in_new_terminal(
1057                SpawnInTerminal {
1058                    id: task::TaskId("ssh-remote".into()),
1059                    full_label: "Install zed over ssh".into(),
1060                    label: "Install zed over ssh".into(),
1061                    command,
1062                    args,
1063                    command_label: ssh_connection_string.clone(),
1064                    cwd: Some(TerminalWorkDir::Ssh {
1065                        ssh_command: ssh_connection_string,
1066                        path: None,
1067                    }),
1068                    env: Default::default(),
1069                    use_new_terminal: true,
1070                    allow_concurrent_runs: false,
1071                    reveal: RevealStrategy::Always,
1072                },
1073                cx,
1074            )
1075        })?
1076        .await?;
1077
1078    terminal
1079        .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
1080        .await;
1081
1082    // 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.
1083    if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
1084        == DevServerStatus::Offline
1085    {
1086        cx.background_executor()
1087            .timer(Duration::from_millis(200))
1088            .await
1089    }
1090
1091    if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
1092        == DevServerStatus::Offline
1093    {
1094        return Err(anyhow!("couldn't reconnect"))?;
1095    }
1096
1097    Ok(())
1098}