dev_servers.rs

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