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