dev_servers.rs

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