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, _| match &result {
196                Ok(dev_server) => {
197                    this.mode = Mode::CreateDevServer(CreateDevServer {
198                        creating: false,
199                        dev_server: Some(dev_server.clone()),
200                    });
201                }
202                Err(_) => {
203                    this.mode = Mode::CreateDevServer(Default::default());
204                }
205            })
206            .log_err();
207            result
208        })
209        .detach_and_prompt_err("Failed to create server", cx, |_, _| None);
210
211        self.mode = Mode::CreateDevServer(CreateDevServer {
212            creating: true,
213            dev_server: None,
214        });
215        cx.notify()
216    }
217
218    fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) {
219        let answer = cx.prompt(
220            gpui::PromptLevel::Destructive,
221            "Are you sure?",
222            Some("This will delete the dev server and all of its remote projects."),
223            &["Delete", "Cancel"],
224        );
225
226        cx.spawn(|this, mut cx| async move {
227            let answer = answer.await?;
228
229            if answer != 0 {
230                return Ok(());
231            }
232
233            let project_ids: Vec<DevServerProjectId> = this.update(&mut cx, |this, cx| {
234                this.dev_server_store.update(cx, |store, _| {
235                    store
236                        .projects_for_server(id)
237                        .into_iter()
238                        .map(|project| project.id)
239                        .collect()
240                })
241            })?;
242
243            this.update(&mut cx, |this, cx| {
244                this.dev_server_store
245                    .update(cx, |store, cx| store.delete_dev_server(id, cx))
246            })?
247            .await?;
248
249            for id in project_ids {
250                WORKSPACE_DB
251                    .delete_workspace_by_dev_server_project_id(id)
252                    .await
253                    .log_err();
254            }
255            Ok(())
256        })
257        .detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None);
258    }
259
260    fn delete_dev_server_project(
261        &mut self,
262        id: DevServerProjectId,
263        path: &str,
264        cx: &mut ViewContext<Self>,
265    ) {
266        let answer = cx.prompt(
267            gpui::PromptLevel::Destructive,
268            format!("Delete \"{}\"?", path).as_str(),
269            Some("This will delete the remote project. You can always re-add it later."),
270            &["Delete", "Cancel"],
271        );
272
273        cx.spawn(|this, mut cx| async move {
274            let answer = answer.await?;
275
276            if answer != 0 {
277                return Ok(());
278            }
279
280            this.update(&mut cx, |this, cx| {
281                this.dev_server_store
282                    .update(cx, |store, cx| store.delete_dev_server_project(id, cx))
283            })?
284            .await?;
285
286            WORKSPACE_DB
287                .delete_workspace_by_dev_server_project_id(id)
288                .await
289                .log_err();
290
291            Ok(())
292        })
293        .detach_and_prompt_err("Failed to delete dev server project", cx, |_, _| None);
294    }
295
296    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
297        match &self.mode {
298            Mode::Default(None) => {}
299            Mode::Default(Some(create_project)) => {
300                self.create_dev_server_project(create_project.dev_server_id, cx);
301            }
302            Mode::CreateDevServer(_) => {
303                self.create_dev_server(cx);
304            }
305        }
306    }
307
308    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
309        match self.mode {
310            Mode::Default(None) => cx.emit(DismissEvent),
311            _ => {
312                self.mode = Mode::Default(None);
313                self.focus_handle(cx).focus(cx);
314                cx.notify();
315            }
316        }
317    }
318
319    fn render_dev_server(
320        &mut self,
321        dev_server: &DevServer,
322        mut create_project: Option<CreateDevServerProject>,
323        cx: &mut ViewContext<Self>,
324    ) -> impl IntoElement {
325        let dev_server_id = dev_server.id;
326        let status = dev_server.status;
327        if create_project
328            .as_ref()
329            .is_some_and(|cp| cp.dev_server_id != dev_server.id)
330        {
331            create_project = None;
332        }
333
334        v_flex()
335            .w_full()
336            .child(
337                h_flex()
338                    .group("dev-server")
339                    .justify_between()
340                    .child(
341                        h_flex()
342                            .gap_2()
343                            .child(
344                                div()
345                                    .id(("status", dev_server.id.0))
346                                    .relative()
347                                    .child(Icon::new(IconName::Server).size(IconSize::Small))
348                                    .child(
349                                        div().absolute().bottom_0().left(rems_from_px(8.0)).child(
350                                            Indicator::dot().color(match status {
351                                                DevServerStatus::Online => Color::Created,
352                                                DevServerStatus::Offline => Color::Hidden,
353                                            }),
354                                        ),
355                                    )
356                                    .tooltip(move |cx| {
357                                        Tooltip::text(
358                                            match status {
359                                                DevServerStatus::Online => "Online",
360                                                DevServerStatus::Offline => "Offline",
361                                            },
362                                            cx,
363                                        )
364                                    }),
365                            )
366                            .child(dev_server.name.clone())
367                            .child(
368                                h_flex()
369                                    .visible_on_hover("dev-server")
370                                    .gap_1()
371                                    .child(
372                                        IconButton::new("edit-dev-server", IconName::Pencil)
373                                            .disabled(true) //TODO implement this on the collab side
374                                            .tooltip(|cx| {
375                                                Tooltip::text("Coming Soon - Edit dev server", cx)
376                                            }),
377                                    )
378                                    .child({
379                                        let dev_server_id = dev_server.id;
380                                        IconButton::new("remove-dev-server", IconName::Trash)
381                                            .on_click(cx.listener(move |this, _, cx| {
382                                                this.delete_dev_server(dev_server_id, cx)
383                                            }))
384                                            .tooltip(|cx| Tooltip::text("Remove dev server", cx))
385                                    }),
386                            ),
387                    )
388                    .child(
389                        h_flex().gap_1().child(
390                            IconButton::new(
391                                ("add-remote-project", dev_server_id.0),
392                                IconName::Plus,
393                            )
394                            .tooltip(|cx| Tooltip::text("Add a remote project", cx))
395                            .on_click(cx.listener(
396                                move |this, _, cx| {
397                                    if let Mode::Default(project) = &mut this.mode {
398                                        *project = Some(CreateDevServerProject {
399                                            dev_server_id,
400                                            creating: false,
401                                        });
402                                    }
403                                    this.project_path_input.read(cx).focus_handle(cx).focus(cx);
404                                    cx.notify();
405                                },
406                            )),
407                        ),
408                    ),
409            )
410            .child(
411                v_flex()
412                    .w_full()
413                    .bg(cx.theme().colors().title_bar_background) // todo: this should be distinct
414                    .border()
415                    .border_color(cx.theme().colors().border_variant)
416                    .rounded_md()
417                    .my_1()
418                    .py_0p5()
419                    .px_3()
420                    .child(
421                        List::new()
422                            .empty_message("No projects.")
423                            .children(
424                                self.dev_server_store
425                                    .read(cx)
426                                    .projects_for_server(dev_server.id)
427                                    .iter()
428                                    .map(|p| self.render_dev_server_project(p, cx)),
429                            )
430                            .when_some(create_project, |el, create_project| {
431                                el.child(self.render_create_new_project(&create_project, cx))
432                            }),
433                    ),
434            )
435    }
436
437    fn render_create_new_project(
438        &mut self,
439        create_project: &CreateDevServerProject,
440        _: &mut ViewContext<Self>,
441    ) -> impl IntoElement {
442        ListItem::new("create-remote-project")
443            .start_slot(Icon::new(IconName::FileTree).color(Color::Muted))
444            .child(self.project_path_input.clone())
445            .child(
446                div()
447                    .w(IconSize::Medium.rems())
448                    .when(create_project.creating, |el| {
449                        el.child(
450                            Icon::new(IconName::ArrowCircle)
451                                .size(IconSize::Medium)
452                                .with_animation(
453                                    "arrow-circle",
454                                    Animation::new(Duration::from_secs(2)).repeat(),
455                                    |icon, delta| {
456                                        icon.transform(Transformation::rotate(percentage(delta)))
457                                    },
458                                ),
459                        )
460                    }),
461            )
462    }
463
464    fn render_dev_server_project(
465        &mut self,
466        project: &DevServerProject,
467        cx: &mut ViewContext<Self>,
468    ) -> impl IntoElement {
469        let dev_server_project_id = project.id;
470        let project_id = project.project_id;
471        let is_online = project_id.is_some();
472        let project_path = project.path.clone();
473
474        ListItem::new(("remote-project", dev_server_project_id.0))
475            .start_slot(Icon::new(IconName::FileTree).when(!is_online, |icon| icon.color(Color::Muted)))
476            .child(
477                    Label::new(project.path.clone())
478            )
479            .on_click(cx.listener(move |_, _, cx| {
480                if let Some(project_id) = project_id {
481                    if let Some(app_state) = AppState::global(cx).upgrade() {
482                        workspace::join_dev_server_project(project_id, app_state, None, cx)
483                            .detach_and_prompt_err("Could not join project", cx, |_, _| None)
484                    }
485                } else {
486                    cx.spawn(|_, mut cx| async move {
487                        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();
488                    }).detach();
489                }
490            }))
491            .end_hover_slot::<AnyElement>(Some(IconButton::new("remove-remote-project", IconName::Trash)
492                .on_click(cx.listener(move |this, _, cx| {
493                    this.delete_dev_server_project(dev_server_project_id, &project_path, cx)
494                }))
495                .tooltip(|cx| Tooltip::text("Delete remote project", cx)).into_any_element()))
496    }
497
498    fn render_create_dev_server(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
499        let Mode::CreateDevServer(CreateDevServer {
500            creating,
501            dev_server,
502        }) = &self.mode
503        else {
504            unreachable!()
505        };
506
507        self.dev_server_name_input.update(cx, |input, cx| {
508            input.set_disabled(*creating || dev_server.is_some(), cx);
509        });
510
511        v_flex()
512            .id("scroll-container")
513            .h_full()
514            .overflow_y_scroll()
515            .track_scroll(&self.scroll_handle)
516            .px_1()
517            .pt_0p5()
518            .gap_px()
519            .child(
520                ModalHeader::new("remote-projects")
521                    .show_back_button(true)
522                    .child(Headline::new("New dev server").size(HeadlineSize::Small)),
523            )
524            .child(
525                ModalContent::new().child(
526                    v_flex()
527                        .w_full()
528                        .child(
529                            h_flex()
530                                .pb_2()
531                                .items_end()
532                                .w_full()
533                                .px_2()
534                                .border_b_1()
535                                .border_color(cx.theme().colors().border)
536                                .child(
537                                    div()
538                                        .pl_2()
539                                        .max_w(rems(16.))
540                                        .child(self.dev_server_name_input.clone()),
541                                )
542                                .child(
543                                    div()
544                                        .pl_1()
545                                        .pb(px(3.))
546                                        .when(!*creating && dev_server.is_none(), |div| {
547                                            div.child(Button::new("create-dev-server", "Create").on_click(
548                                                cx.listener(move |this, _, cx| {
549                                                    this.create_dev_server(cx);
550                                                }),
551                                            ))
552                                        })
553                                        .when(*creating && dev_server.is_none(), |div| {
554                                            div.child(
555                                                Button::new("create-dev-server", "Creating...")
556                                                    .disabled(true),
557                                            )
558                                        }),
559                                )
560                        )
561                        .when(dev_server.is_none(), |div| {
562                            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))
563                        })
564                        .when_some(dev_server.clone(), |div, dev_server| {
565                            let status = self
566                                .dev_server_store
567                                .read(cx)
568                                .dev_server_status(DevServerId(dev_server.dev_server_id));
569
570                            let instructions = SharedString::from(format!(
571                                "zed --dev-server-token {}",
572                                dev_server.access_token
573                            ));
574                            div.child(
575                                v_flex()
576                                    .pl_2()
577                                    .pt_2()
578                                    .gap_2()
579                                    .child(
580                                        h_flex().justify_between().w_full()
581                                            .child(Label::new(format!(
582                                                    "Please log into `{}` and run:",
583                                                    dev_server.name
584                                            )))
585                                            .child(
586                                                Button::new("copy-access-token", "Copy Instructions")
587                                                    .icon(Some(IconName::Copy))
588                                                    .icon_size(IconSize::Small)
589                                                    .on_click({
590                                                        let instructions = instructions.clone();
591                                                        cx.listener(move |_, _, cx| {
592                                                        cx.write_to_clipboard(ClipboardItem::new(
593                                                            instructions.to_string(),
594                                                        ))
595                                                    })})
596                                            )
597                                    )
598                                    .child(
599                                        v_flex()
600                                        .w_full()
601                                        .bg(cx.theme().colors().title_bar_background) // todo: this should be distinct
602                                        .border()
603                                        .border_color(cx.theme().colors().border_variant)
604                                        .rounded_md()
605                                        .my_1()
606                                        .py_0p5()
607                                        .px_3()
608                                        .font_family(ThemeSettings::get_global(cx).buffer_font.family.clone())
609                                        .child(Label::new(instructions))
610                                    )
611                                    .when(status == DevServerStatus::Offline, |this| {
612                                        this.child(
613
614                                        h_flex()
615                                            .gap_2()
616                                            .child(
617                                                Icon::new(IconName::ArrowCircle)
618                                                    .size(IconSize::Medium)
619                                                    .with_animation(
620                                                        "arrow-circle",
621                                                        Animation::new(Duration::from_secs(2)).repeat(),
622                                                        |icon, delta| {
623                                                            icon.transform(Transformation::rotate(percentage(delta)))
624                                                        },
625                                                    ),
626                                            )
627                                            .child(
628                                                Label::new("Waiting for connection…"),
629                                            )
630                                        )
631                                    })
632                                    .when(status == DevServerStatus::Online, |this| {
633                                        this.child(Label::new("🎊 Connection established!"))
634                                            .child(
635                                                h_flex().justify_end().child(
636                                                    Button::new("done", "Done").on_click(cx.listener(
637                                                        |_, _, cx| {
638                                                            cx.dispatch_action(menu::Cancel.boxed_clone())
639                                                        },
640                                                    ))
641                                                ),
642                                            )
643                                    }),
644                            )
645                        }),
646                )
647            )
648    }
649
650    fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
651        let dev_servers = self.dev_server_store.read(cx).dev_servers();
652
653        let Mode::Default(create_dev_server_project) = &self.mode else {
654            unreachable!()
655        };
656        let create_dev_server_project = create_dev_server_project.clone();
657
658        v_flex()
659            .id("scroll-container")
660            .h_full()
661            .overflow_y_scroll()
662            .track_scroll(&self.scroll_handle)
663            .px_1()
664            .pt_0p5()
665            .gap_px()
666            .child(
667                ModalHeader::new("remote-projects")
668                    .show_dismiss_button(true)
669                    .child(Headline::new("Remote Projects").size(HeadlineSize::Small)),
670            )
671            .child(
672                ModalContent::new().child(
673                    List::new()
674                        .empty_message("No dev servers registered.")
675                        .header(Some(
676                            ListHeader::new("Dev Servers").end_slot(
677                                Button::new("register-dev-server-button", "New Server")
678                                    .icon(IconName::Plus)
679                                    .icon_position(IconPosition::Start)
680                                    .tooltip(|cx| Tooltip::text("Register a new dev server", cx))
681                                    .on_click(cx.listener(|this, _, cx| {
682                                        this.mode = Mode::CreateDevServer(Default::default());
683
684                                        this.dev_server_name_input.update(cx, |input, cx| {
685                                            input.editor().update(cx, |editor, cx| {
686                                                editor.set_text("", cx);
687                                            });
688                                            input.focus_handle(cx).focus(cx)
689                                        });
690
691                                        cx.notify();
692                                    })),
693                            ),
694                        ))
695                        .children(dev_servers.iter().map(|dev_server| {
696                            self.render_dev_server(
697                                dev_server,
698                                create_dev_server_project.clone(),
699                                cx,
700                            )
701                            .into_any_element()
702                        })),
703                ),
704            )
705    }
706
707    // fn render_create_dev_server_project(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
708    //     let Mode::CreateDevServerProject(CreateDevServerProject {
709    //         dev_server_id,
710    //         creating,
711    //         dev_server_project,
712    //     }) = &self.mode
713    //     else {
714    //         unreachable!()
715    //     };
716
717    //     let dev_server = self
718    //         .dev_server_store
719    //         .read(cx)
720    //         .dev_server(*dev_server_id)
721    //         .cloned();
722
723    //     let (dev_server_name, dev_server_status) = dev_server
724    //         .map(|server| (server.name, server.status))
725    //         .unwrap_or((SharedString::from(""), DevServerStatus::Offline));
726
727    //     v_flex()
728    //         .px_1()
729    //         .pt_0p5()
730    //         .gap_px()
731    //         .child(
732    //             v_flex().py_0p5().px_1().child(
733    //                 h_flex()
734    //                     .px_1()
735    //                     .py_0p5()
736    //                     .child(
737    //                         IconButton::new("back", IconName::ArrowLeft)
738    //                             .style(ButtonStyle::Transparent)
739    //                             .on_click(cx.listener(|_, _: &gpui::ClickEvent, cx| {
740    //                                 cx.dispatch_action(menu::Cancel.boxed_clone())
741    //                             })),
742    //                     )
743    //                     .child(Headline::new("Add remote project").size(HeadlineSize::Small)),
744    //             ),
745    //         )
746    //         .child(
747    //             h_flex()
748    //                 .ml_5()
749    //                 .gap_2()
750    //                 .child(
751    //                     div()
752    //                         .id(("status", dev_server_id.0))
753    //                         .relative()
754    //                         .child(Icon::new(IconName::Server))
755    //                         .child(div().absolute().bottom_0().left(rems_from_px(12.0)).child(
756    //                             Indicator::dot().color(match dev_server_status {
757    //                                 DevServerStatus::Online => Color::Created,
758    //                                 DevServerStatus::Offline => Color::Hidden,
759    //                             }),
760    //                         ))
761    //                         .tooltip(move |cx| {
762    //                             Tooltip::text(
763    //                                 match dev_server_status {
764    //                                     DevServerStatus::Online => "Online",
765    //                                     DevServerStatus::Offline => "Offline",
766    //                                 },
767    //                                 cx,
768    //                             )
769    //                         }),
770    //                 )
771    //                 .child(dev_server_name.clone()),
772    //         )
773    //         .child(
774    //             h_flex()
775    //                 .ml_5()
776    //                 .gap_2()
777    //                 .child(self.project_path_input.clone())
778    //                 .when(!*creating && dev_server_project.is_none(), |div| {
779    //                     div.child(Button::new("create-remote-server", "Create").on_click({
780    //                         let dev_server_id = *dev_server_id;
781    //                         cx.listener(move |this, _, cx| {
782    //                             this.create_dev_server_project(dev_server_id, cx)
783    //                         })
784    //                     }))
785    //                 })
786    //                 .when(*creating, |div| {
787    //                     div.child(Button::new("create-dev-server", "Creating...").disabled(true))
788    //                 }),
789    //         )
790    //         .when_some(dev_server_project.clone(), |div, dev_server_project| {
791    //             let status = self
792    //                 .dev_server_store
793    //                 .read(cx)
794    //                 .dev_server_project(DevServerProjectId(dev_server_project.id))
795    //                 .map(|project| {
796    //                     if project.project_id.is_some() {
797    //                         DevServerStatus::Online
798    //                     } else {
799    //                         DevServerStatus::Offline
800    //                     }
801    //                 })
802    //                 .unwrap_or(DevServerStatus::Offline);
803    //             div.child(
804    //                 v_flex()
805    //                     .ml_5()
806    //                     .ml_8()
807    //                     .gap_2()
808    //                     .when(status == DevServerStatus::Offline, |this| {
809    //                         this.child(Label::new("Waiting for project..."))
810    //                     })
811    //                     .when(status == DevServerStatus::Online, |this| {
812    //                         this.child(Label::new("Project online! 🎊")).child(
813    //                             Button::new("done", "Done").on_click(cx.listener(|_, _, cx| {
814    //                                 cx.dispatch_action(menu::Cancel.boxed_clone())
815    //                             })),
816    //                         )
817    //                     }),
818    //             )
819    //         })
820    // }
821}
822impl ModalView for DevServerProjects {}
823
824impl FocusableView for DevServerProjects {
825    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
826        self.focus_handle.clone()
827    }
828}
829
830impl EventEmitter<DismissEvent> for DevServerProjects {}
831
832impl Render for DevServerProjects {
833    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
834        div()
835            .track_focus(&self.focus_handle)
836            .elevation_3(cx)
837            .key_context("DevServerModal")
838            .on_action(cx.listener(Self::cancel))
839            .on_action(cx.listener(Self::confirm))
840            .on_mouse_down_out(cx.listener(|this, _, cx| {
841                if matches!(this.mode, Mode::Default(None)) {
842                    cx.emit(DismissEvent)
843                }
844            }))
845            .pb_4()
846            .w(rems(34.))
847            .min_h(rems(20.))
848            .max_h(rems(40.))
849            .child(match &self.mode {
850                Mode::Default(_) => self.render_default(cx).into_any_element(),
851                Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(),
852            })
853    }
854}