dev_servers.rs

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