lib.rs

   1use std::path::Path;
   2
   3use gpui::AppContext;
   4use gpui::Entity;
   5use gpui::Task;
   6use http_client::anyhow;
   7use picker::Picker;
   8use picker::PickerDelegate;
   9use settings::RegisterSetting;
  10use settings::Settings;
  11use std::collections::HashMap;
  12use std::collections::HashSet;
  13use std::fmt::Debug;
  14use std::fmt::Display;
  15use std::sync::Arc;
  16use ui::ActiveTheme;
  17use ui::Button;
  18use ui::Clickable;
  19use ui::FluentBuilder;
  20use ui::KeyBinding;
  21use ui::StatefulInteractiveElement;
  22use ui::Switch;
  23use ui::ToggleState;
  24use ui::Tooltip;
  25use ui::h_flex;
  26use ui::rems_from_px;
  27use ui::v_flex;
  28
  29use gpui::{Action, DismissEvent, EventEmitter, FocusHandle, Focusable, RenderOnce, WeakEntity};
  30use serde::Deserialize;
  31use ui::{
  32    AnyElement, App, Color, CommonAnimationExt, Context, Headline, HeadlineSize, Icon, IconName,
  33    InteractiveElement, IntoElement, Label, ListItem, ListSeparator, ModalHeader, Navigable,
  34    NavigableEntry, ParentElement, Render, Styled, StyledExt, Toggleable, Window, div, rems,
  35};
  36use util::ResultExt;
  37use util::rel_path::RelPath;
  38use workspace::{ModalView, Workspace, with_active_or_new_workspace};
  39
  40use futures::AsyncReadExt;
  41use http::Request;
  42use http_client::{AsyncBody, HttpClient};
  43
  44mod devcontainer_api;
  45
  46use devcontainer_api::ensure_devcontainer_cli;
  47use devcontainer_api::read_devcontainer_configuration;
  48
  49use crate::devcontainer_api::DevContainerError;
  50use crate::devcontainer_api::apply_dev_container_template;
  51
  52pub use devcontainer_api::{
  53    DevContainerConfig, find_configs_in_snapshot, find_devcontainer_configs,
  54    start_dev_container_with_config,
  55};
  56
  57pub struct DevContainerContext {
  58    pub project_directory: Arc<Path>,
  59    pub use_podman: bool,
  60    pub node_runtime: node_runtime::NodeRuntime,
  61}
  62
  63impl DevContainerContext {
  64    pub fn from_workspace(workspace: &Workspace, cx: &App) -> Option<Self> {
  65        let project_directory = workspace.project().read(cx).active_project_directory(cx)?;
  66        let use_podman = DevContainerSettings::get_global(cx).use_podman;
  67        let node_runtime = workspace.app_state().node_runtime.clone();
  68        Some(Self {
  69            project_directory,
  70            use_podman,
  71            node_runtime,
  72        })
  73    }
  74}
  75
  76#[derive(RegisterSetting)]
  77struct DevContainerSettings {
  78    use_podman: bool,
  79}
  80
  81pub fn use_podman(cx: &App) -> bool {
  82    DevContainerSettings::get_global(cx).use_podman
  83}
  84
  85impl Settings for DevContainerSettings {
  86    fn from_settings(content: &settings::SettingsContent) -> Self {
  87        Self {
  88            use_podman: content.remote.use_podman.unwrap_or(false),
  89        }
  90    }
  91}
  92
  93#[derive(PartialEq, Clone, Deserialize, Default, Action)]
  94#[action(namespace = projects)]
  95#[serde(deny_unknown_fields)]
  96struct InitializeDevContainer;
  97
  98pub fn init(cx: &mut App) {
  99    cx.on_action(|_: &InitializeDevContainer, cx| {
 100        with_active_or_new_workspace(cx, move |workspace, window, cx| {
 101            let weak_entity = cx.weak_entity();
 102            workspace.toggle_modal(window, cx, |window, cx| {
 103                DevContainerModal::new(weak_entity, window, cx)
 104            });
 105        });
 106    });
 107}
 108
 109#[derive(Clone)]
 110struct TemplateEntry {
 111    template: DevContainerTemplate,
 112    options_selected: HashMap<String, String>,
 113    current_option_index: usize,
 114    current_option: Option<TemplateOptionSelection>,
 115    features_selected: HashSet<DevContainerFeature>,
 116}
 117
 118#[derive(Clone)]
 119struct FeatureEntry {
 120    feature: DevContainerFeature,
 121    toggle_state: ToggleState,
 122}
 123
 124#[derive(Clone)]
 125struct TemplateOptionSelection {
 126    option_name: String,
 127    description: String,
 128    navigable_options: Vec<(String, NavigableEntry)>,
 129}
 130
 131impl Eq for TemplateEntry {}
 132impl PartialEq for TemplateEntry {
 133    fn eq(&self, other: &Self) -> bool {
 134        self.template == other.template
 135    }
 136}
 137impl Debug for TemplateEntry {
 138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 139        f.debug_struct("TemplateEntry")
 140            .field("template", &self.template)
 141            .finish()
 142    }
 143}
 144
 145impl Eq for FeatureEntry {}
 146impl PartialEq for FeatureEntry {
 147    fn eq(&self, other: &Self) -> bool {
 148        self.feature == other.feature
 149    }
 150}
 151
 152impl Debug for FeatureEntry {
 153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 154        f.debug_struct("FeatureEntry")
 155            .field("feature", &self.feature)
 156            .finish()
 157    }
 158}
 159
 160#[derive(Debug, Clone, PartialEq, Eq)]
 161enum DevContainerState {
 162    Initial,
 163    QueryingTemplates,
 164    TemplateQueryReturned(Result<Vec<TemplateEntry>, String>),
 165    QueryingFeatures(TemplateEntry),
 166    FeaturesQueryReturned(TemplateEntry),
 167    UserOptionsSpecifying(TemplateEntry),
 168    ConfirmingWriteDevContainer(TemplateEntry),
 169    TemplateWriteFailed(DevContainerError),
 170}
 171
 172#[derive(Debug, Clone)]
 173enum DevContainerMessage {
 174    SearchTemplates,
 175    TemplatesRetrieved(Vec<DevContainerTemplate>),
 176    ErrorRetrievingTemplates(String),
 177    TemplateSelected(TemplateEntry),
 178    TemplateOptionsSpecified(TemplateEntry),
 179    TemplateOptionsCompleted(TemplateEntry),
 180    FeaturesRetrieved(Vec<DevContainerFeature>),
 181    FeaturesSelected(TemplateEntry),
 182    NeedConfirmWriteDevContainer(TemplateEntry),
 183    ConfirmWriteDevContainer(TemplateEntry),
 184    FailedToWriteTemplate(DevContainerError),
 185    GoBack,
 186}
 187
 188struct DevContainerModal {
 189    workspace: WeakEntity<Workspace>,
 190    picker: Option<Entity<Picker<TemplatePickerDelegate>>>,
 191    features_picker: Option<Entity<Picker<FeaturePickerDelegate>>>,
 192    focus_handle: FocusHandle,
 193    confirm_entry: NavigableEntry,
 194    back_entry: NavigableEntry,
 195    state: DevContainerState,
 196}
 197
 198struct TemplatePickerDelegate {
 199    selected_index: usize,
 200    placeholder_text: String,
 201    stateful_modal: WeakEntity<DevContainerModal>,
 202    candidate_templates: Vec<TemplateEntry>,
 203    matching_indices: Vec<usize>,
 204    on_confirm: Box<
 205        dyn FnMut(
 206            TemplateEntry,
 207            &mut DevContainerModal,
 208            &mut Window,
 209            &mut Context<DevContainerModal>,
 210        ),
 211    >,
 212}
 213
 214impl TemplatePickerDelegate {
 215    fn new(
 216        placeholder_text: String,
 217        stateful_modal: WeakEntity<DevContainerModal>,
 218        elements: Vec<TemplateEntry>,
 219        on_confirm: Box<
 220            dyn FnMut(
 221                TemplateEntry,
 222                &mut DevContainerModal,
 223                &mut Window,
 224                &mut Context<DevContainerModal>,
 225            ),
 226        >,
 227    ) -> Self {
 228        Self {
 229            selected_index: 0,
 230            placeholder_text,
 231            stateful_modal,
 232            candidate_templates: elements,
 233            matching_indices: Vec::new(),
 234            on_confirm,
 235        }
 236    }
 237}
 238
 239impl PickerDelegate for TemplatePickerDelegate {
 240    type ListItem = AnyElement;
 241
 242    fn match_count(&self) -> usize {
 243        self.matching_indices.len()
 244    }
 245
 246    fn selected_index(&self) -> usize {
 247        self.selected_index
 248    }
 249
 250    fn set_selected_index(
 251        &mut self,
 252        ix: usize,
 253        _window: &mut Window,
 254        _cx: &mut Context<picker::Picker<Self>>,
 255    ) {
 256        self.selected_index = ix;
 257    }
 258
 259    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 260        self.placeholder_text.clone().into()
 261    }
 262
 263    fn update_matches(
 264        &mut self,
 265        query: String,
 266        _window: &mut Window,
 267        _cx: &mut Context<picker::Picker<Self>>,
 268    ) -> gpui::Task<()> {
 269        self.matching_indices = self
 270            .candidate_templates
 271            .iter()
 272            .enumerate()
 273            .filter(|(_, template_entry)| {
 274                template_entry
 275                    .template
 276                    .id
 277                    .to_lowercase()
 278                    .contains(&query.to_lowercase())
 279                    || template_entry
 280                        .template
 281                        .name
 282                        .to_lowercase()
 283                        .contains(&query.to_lowercase())
 284            })
 285            .map(|(ix, _)| ix)
 286            .collect();
 287
 288        self.selected_index = std::cmp::min(
 289            self.selected_index,
 290            self.matching_indices.len().saturating_sub(1),
 291        );
 292        Task::ready(())
 293    }
 294
 295    fn confirm(
 296        &mut self,
 297        _secondary: bool,
 298        window: &mut Window,
 299        cx: &mut Context<picker::Picker<Self>>,
 300    ) {
 301        let fun = &mut self.on_confirm;
 302
 303        self.stateful_modal
 304            .update(cx, |modal, cx| {
 305                fun(
 306                    self.candidate_templates[self.matching_indices[self.selected_index]].clone(),
 307                    modal,
 308                    window,
 309                    cx,
 310                );
 311            })
 312            .ok();
 313    }
 314
 315    fn dismissed(&mut self, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
 316        self.stateful_modal
 317            .update(cx, |modal, cx| {
 318                modal.dismiss(&menu::Cancel, window, cx);
 319            })
 320            .ok();
 321    }
 322
 323    fn render_match(
 324        &self,
 325        ix: usize,
 326        selected: bool,
 327        _window: &mut Window,
 328        _cx: &mut Context<picker::Picker<Self>>,
 329    ) -> Option<Self::ListItem> {
 330        let Some(template_entry) = self.candidate_templates.get(self.matching_indices[ix]) else {
 331            return None;
 332        };
 333        Some(
 334            ListItem::new("li-template-match")
 335                .inset(true)
 336                .spacing(ui::ListItemSpacing::Sparse)
 337                .start_slot(Icon::new(IconName::Box))
 338                .toggle_state(selected)
 339                .child(Label::new(template_entry.template.name.clone()))
 340                .into_any_element(),
 341        )
 342    }
 343
 344    fn render_footer(
 345        &self,
 346        _window: &mut Window,
 347        cx: &mut Context<Picker<Self>>,
 348    ) -> Option<AnyElement> {
 349        Some(
 350            h_flex()
 351                .w_full()
 352                .p_1p5()
 353                .gap_1()
 354                .justify_start()
 355                .border_t_1()
 356                .border_color(cx.theme().colors().border_variant)
 357                .child(
 358                    Button::new("run-action", "Continue")
 359                        .key_binding(
 360                            KeyBinding::for_action(&menu::Confirm, cx)
 361                                .map(|kb| kb.size(rems_from_px(12.))),
 362                        )
 363                        .on_click(|_, window, cx| {
 364                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)
 365                        }),
 366                )
 367                .into_any_element(),
 368        )
 369    }
 370}
 371
 372struct FeaturePickerDelegate {
 373    selected_index: usize,
 374    placeholder_text: String,
 375    stateful_modal: WeakEntity<DevContainerModal>,
 376    candidate_features: Vec<FeatureEntry>,
 377    template_entry: TemplateEntry,
 378    matching_indices: Vec<usize>,
 379    on_confirm: Box<
 380        dyn FnMut(
 381            TemplateEntry,
 382            &mut DevContainerModal,
 383            &mut Window,
 384            &mut Context<DevContainerModal>,
 385        ),
 386    >,
 387}
 388
 389impl FeaturePickerDelegate {
 390    fn new(
 391        placeholder_text: String,
 392        stateful_modal: WeakEntity<DevContainerModal>,
 393        candidate_features: Vec<FeatureEntry>,
 394        template_entry: TemplateEntry,
 395        on_confirm: Box<
 396            dyn FnMut(
 397                TemplateEntry,
 398                &mut DevContainerModal,
 399                &mut Window,
 400                &mut Context<DevContainerModal>,
 401            ),
 402        >,
 403    ) -> Self {
 404        Self {
 405            selected_index: 0,
 406            placeholder_text,
 407            stateful_modal,
 408            candidate_features,
 409            template_entry,
 410            matching_indices: Vec::new(),
 411            on_confirm,
 412        }
 413    }
 414}
 415
 416impl PickerDelegate for FeaturePickerDelegate {
 417    type ListItem = AnyElement;
 418
 419    fn match_count(&self) -> usize {
 420        self.matching_indices.len()
 421    }
 422
 423    fn selected_index(&self) -> usize {
 424        self.selected_index
 425    }
 426
 427    fn set_selected_index(
 428        &mut self,
 429        ix: usize,
 430        _window: &mut Window,
 431        _cx: &mut Context<Picker<Self>>,
 432    ) {
 433        self.selected_index = ix;
 434    }
 435
 436    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 437        self.placeholder_text.clone().into()
 438    }
 439
 440    fn update_matches(
 441        &mut self,
 442        query: String,
 443        _window: &mut Window,
 444        _cx: &mut Context<Picker<Self>>,
 445    ) -> Task<()> {
 446        self.matching_indices = self
 447            .candidate_features
 448            .iter()
 449            .enumerate()
 450            .filter(|(_, feature_entry)| {
 451                feature_entry
 452                    .feature
 453                    .id
 454                    .to_lowercase()
 455                    .contains(&query.to_lowercase())
 456                    || feature_entry
 457                        .feature
 458                        .name
 459                        .to_lowercase()
 460                        .contains(&query.to_lowercase())
 461            })
 462            .map(|(ix, _)| ix)
 463            .collect();
 464        self.selected_index = std::cmp::min(
 465            self.selected_index,
 466            self.matching_indices.len().saturating_sub(1),
 467        );
 468        Task::ready(())
 469    }
 470
 471    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
 472        if secondary {
 473            self.stateful_modal
 474                .update(cx, |modal, cx| {
 475                    (self.on_confirm)(self.template_entry.clone(), modal, window, cx)
 476                })
 477                .ok();
 478        } else {
 479            let current = &mut self.candidate_features[self.matching_indices[self.selected_index]];
 480            current.toggle_state = match current.toggle_state {
 481                ToggleState::Selected => {
 482                    self.template_entry
 483                        .features_selected
 484                        .remove(&current.feature);
 485                    ToggleState::Unselected
 486                }
 487                _ => {
 488                    self.template_entry
 489                        .features_selected
 490                        .insert(current.feature.clone());
 491                    ToggleState::Selected
 492                }
 493            };
 494        }
 495    }
 496
 497    fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
 498        self.stateful_modal
 499            .update(cx, |modal, cx| {
 500                modal.dismiss(&menu::Cancel, window, cx);
 501            })
 502            .ok();
 503    }
 504
 505    fn render_match(
 506        &self,
 507        ix: usize,
 508        selected: bool,
 509        _window: &mut Window,
 510        _cx: &mut Context<Picker<Self>>,
 511    ) -> Option<Self::ListItem> {
 512        let feature_entry = self.candidate_features[self.matching_indices[ix]].clone();
 513
 514        Some(
 515            ListItem::new("li-what")
 516                .inset(true)
 517                .toggle_state(selected)
 518                .start_slot(Switch::new(
 519                    feature_entry.feature.id.clone(),
 520                    feature_entry.toggle_state,
 521                ))
 522                .child(Label::new(feature_entry.feature.name))
 523                .into_any_element(),
 524        )
 525    }
 526
 527    fn render_footer(
 528        &self,
 529        _window: &mut Window,
 530        cx: &mut Context<Picker<Self>>,
 531    ) -> Option<AnyElement> {
 532        Some(
 533            h_flex()
 534                .w_full()
 535                .p_1p5()
 536                .gap_1()
 537                .justify_start()
 538                .border_t_1()
 539                .border_color(cx.theme().colors().border_variant)
 540                .child(
 541                    Button::new("run-action", "Select Feature")
 542                        .key_binding(
 543                            KeyBinding::for_action(&menu::Confirm, cx)
 544                                .map(|kb| kb.size(rems_from_px(12.))),
 545                        )
 546                        .on_click(|_, window, cx| {
 547                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)
 548                        }),
 549                )
 550                .child(
 551                    Button::new("run-action-secondary", "Confirm Selections")
 552                        .key_binding(
 553                            KeyBinding::for_action(&menu::SecondaryConfirm, cx)
 554                                .map(|kb| kb.size(rems_from_px(12.))),
 555                        )
 556                        .on_click(|_, window, cx| {
 557                            window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
 558                        }),
 559                )
 560                .into_any_element(),
 561        )
 562    }
 563}
 564
 565impl DevContainerModal {
 566    fn new(workspace: WeakEntity<Workspace>, _window: &mut Window, cx: &mut App) -> Self {
 567        DevContainerModal {
 568            workspace,
 569            picker: None,
 570            features_picker: None,
 571            state: DevContainerState::Initial,
 572            focus_handle: cx.focus_handle(),
 573            confirm_entry: NavigableEntry::focusable(cx),
 574            back_entry: NavigableEntry::focusable(cx),
 575        }
 576    }
 577
 578    fn render_initial(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 579        let mut view = Navigable::new(
 580            div()
 581                .p_1()
 582                .child(
 583                    div().track_focus(&self.focus_handle).child(
 584                        ModalHeader::new().child(
 585                            Headline::new("Create Dev Container").size(HeadlineSize::XSmall),
 586                        ),
 587                    ),
 588                )
 589                .child(ListSeparator)
 590                .child(
 591                    div()
 592                        .track_focus(&self.confirm_entry.focus_handle)
 593                        .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
 594                            this.accept_message(DevContainerMessage::SearchTemplates, window, cx);
 595                        }))
 596                        .child(
 597                            ListItem::new("li-search-containers")
 598                                .inset(true)
 599                                .spacing(ui::ListItemSpacing::Sparse)
 600                                .start_slot(
 601                                    Icon::new(IconName::MagnifyingGlass).color(Color::Muted),
 602                                )
 603                                .toggle_state(
 604                                    self.confirm_entry.focus_handle.contains_focused(window, cx),
 605                                )
 606                                .on_click(cx.listener(|this, _, window, cx| {
 607                                    this.accept_message(
 608                                        DevContainerMessage::SearchTemplates,
 609                                        window,
 610                                        cx,
 611                                    );
 612                                    cx.notify();
 613                                }))
 614                                .child(Label::new("Search for Dev Container Templates")),
 615                        ),
 616                )
 617                .into_any_element(),
 618        );
 619        view = view.entry(self.confirm_entry.clone());
 620        view.render(window, cx).into_any_element()
 621    }
 622
 623    fn render_error(
 624        &self,
 625        error_title: String,
 626        error: impl Display,
 627        _window: &mut Window,
 628        _cx: &mut Context<Self>,
 629    ) -> AnyElement {
 630        v_flex()
 631            .p_1()
 632            .child(div().track_focus(&self.focus_handle).child(
 633                ModalHeader::new().child(Headline::new(error_title).size(HeadlineSize::XSmall)),
 634            ))
 635            .child(ListSeparator)
 636            .child(
 637                v_flex()
 638                    .child(Label::new(format!("{}", error)))
 639                    .whitespace_normal(),
 640            )
 641            .into_any_element()
 642    }
 643
 644    fn render_retrieved_templates(
 645        &self,
 646        window: &mut Window,
 647        cx: &mut Context<Self>,
 648    ) -> AnyElement {
 649        if let Some(picker) = &self.picker {
 650            let picker_element = div()
 651                .track_focus(&self.focus_handle(cx))
 652                .child(picker.clone().into_any_element())
 653                .into_any_element();
 654            picker.focus_handle(cx).focus(window, cx);
 655            picker_element
 656        } else {
 657            div().into_any_element()
 658        }
 659    }
 660
 661    fn render_user_options_specifying(
 662        &self,
 663        template_entry: TemplateEntry,
 664        window: &mut Window,
 665        cx: &mut Context<Self>,
 666    ) -> AnyElement {
 667        let Some(next_option_entries) = &template_entry.current_option else {
 668            return div().into_any_element();
 669        };
 670        let mut view = Navigable::new(
 671            div()
 672                .child(
 673                    div()
 674                        .id("title")
 675                        .tooltip(Tooltip::text(next_option_entries.description.clone()))
 676                        .track_focus(&self.focus_handle)
 677                        .child(
 678                            ModalHeader::new()
 679                                .child(
 680                                    Headline::new("Template Option: ").size(HeadlineSize::XSmall),
 681                                )
 682                                .child(
 683                                    Headline::new(&next_option_entries.option_name)
 684                                        .size(HeadlineSize::XSmall),
 685                                ),
 686                        ),
 687                )
 688                .child(ListSeparator)
 689                .children(
 690                    next_option_entries
 691                        .navigable_options
 692                        .iter()
 693                        .map(|(option, entry)| {
 694                            div()
 695                                .id(format!("li-parent-{}", option))
 696                                .track_focus(&entry.focus_handle)
 697                                .on_action({
 698                                    let mut template = template_entry.clone();
 699                                    template.options_selected.insert(
 700                                        next_option_entries.option_name.clone(),
 701                                        option.clone(),
 702                                    );
 703                                    cx.listener(move |this, _: &menu::Confirm, window, cx| {
 704                                        this.accept_message(
 705                                            DevContainerMessage::TemplateOptionsSpecified(
 706                                                template.clone(),
 707                                            ),
 708                                            window,
 709                                            cx,
 710                                        );
 711                                    })
 712                                })
 713                                .child(
 714                                    ListItem::new(format!("li-option-{}", option))
 715                                        .inset(true)
 716                                        .spacing(ui::ListItemSpacing::Sparse)
 717                                        .toggle_state(
 718                                            entry.focus_handle.contains_focused(window, cx),
 719                                        )
 720                                        .on_click({
 721                                            let mut template = template_entry.clone();
 722                                            template.options_selected.insert(
 723                                                next_option_entries.option_name.clone(),
 724                                                option.clone(),
 725                                            );
 726                                            cx.listener(move |this, _, window, cx| {
 727                                                this.accept_message(
 728                                                    DevContainerMessage::TemplateOptionsSpecified(
 729                                                        template.clone(),
 730                                                    ),
 731                                                    window,
 732                                                    cx,
 733                                                );
 734                                                cx.notify();
 735                                            })
 736                                        })
 737                                        .child(Label::new(option)),
 738                                )
 739                        }),
 740                )
 741                .child(ListSeparator)
 742                .child(
 743                    div()
 744                        .track_focus(&self.back_entry.focus_handle)
 745                        .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
 746                            this.accept_message(DevContainerMessage::GoBack, window, cx);
 747                        }))
 748                        .child(
 749                            ListItem::new("li-goback")
 750                                .inset(true)
 751                                .spacing(ui::ListItemSpacing::Sparse)
 752                                .start_slot(Icon::new(IconName::Return).color(Color::Muted))
 753                                .toggle_state(
 754                                    self.back_entry.focus_handle.contains_focused(window, cx),
 755                                )
 756                                .on_click(cx.listener(|this, _, window, cx| {
 757                                    this.accept_message(DevContainerMessage::GoBack, window, cx);
 758                                    cx.notify();
 759                                }))
 760                                .child(Label::new("Go Back")),
 761                        ),
 762                )
 763                .into_any_element(),
 764        );
 765        for (_, entry) in &next_option_entries.navigable_options {
 766            view = view.entry(entry.clone());
 767        }
 768        view = view.entry(self.back_entry.clone());
 769        view.render(window, cx).into_any_element()
 770    }
 771
 772    fn render_features_query_returned(
 773        &self,
 774        window: &mut Window,
 775        cx: &mut Context<Self>,
 776    ) -> AnyElement {
 777        if let Some(picker) = &self.features_picker {
 778            let picker_element = div()
 779                .track_focus(&self.focus_handle(cx))
 780                .child(picker.clone().into_any_element())
 781                .into_any_element();
 782            picker.focus_handle(cx).focus(window, cx);
 783            picker_element
 784        } else {
 785            div().into_any_element()
 786        }
 787    }
 788
 789    fn render_confirming_write_dev_container(
 790        &self,
 791        template_entry: TemplateEntry,
 792        window: &mut Window,
 793        cx: &mut Context<Self>,
 794    ) -> AnyElement {
 795        Navigable::new(
 796            div()
 797                .child(
 798                    div().track_focus(&self.focus_handle).child(
 799                        ModalHeader::new()
 800                            .icon(Icon::new(IconName::Warning).color(Color::Warning))
 801                            .child(
 802                                Headline::new("Overwrite Existing Configuration?")
 803                                    .size(HeadlineSize::XSmall),
 804                            ),
 805                    ),
 806                )
 807                .child(
 808                    div()
 809                        .track_focus(&self.confirm_entry.focus_handle)
 810                        .on_action({
 811                            let template = template_entry.clone();
 812                            cx.listener(move |this, _: &menu::Confirm, window, cx| {
 813                                this.accept_message(
 814                                    DevContainerMessage::ConfirmWriteDevContainer(template.clone()),
 815                                    window,
 816                                    cx,
 817                                );
 818                            })
 819                        })
 820                        .child(
 821                            ListItem::new("li-search-containers")
 822                                .inset(true)
 823                                .spacing(ui::ListItemSpacing::Sparse)
 824                                .start_slot(Icon::new(IconName::Check).color(Color::Muted))
 825                                .toggle_state(
 826                                    self.confirm_entry.focus_handle.contains_focused(window, cx),
 827                                )
 828                                .on_click(cx.listener(move |this, _, window, cx| {
 829                                    this.accept_message(
 830                                        DevContainerMessage::ConfirmWriteDevContainer(
 831                                            template_entry.clone(),
 832                                        ),
 833                                        window,
 834                                        cx,
 835                                    );
 836                                    cx.notify();
 837                                }))
 838                                .child(Label::new("Overwrite")),
 839                        ),
 840                )
 841                .child(
 842                    div()
 843                        .track_focus(&self.back_entry.focus_handle)
 844                        .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
 845                            this.dismiss(&menu::Cancel, window, cx);
 846                        }))
 847                        .child(
 848                            ListItem::new("li-goback")
 849                                .inset(true)
 850                                .spacing(ui::ListItemSpacing::Sparse)
 851                                .start_slot(Icon::new(IconName::XCircle).color(Color::Muted))
 852                                .toggle_state(
 853                                    self.back_entry.focus_handle.contains_focused(window, cx),
 854                                )
 855                                .on_click(cx.listener(|this, _, window, cx| {
 856                                    this.dismiss(&menu::Cancel, window, cx);
 857                                    cx.notify();
 858                                }))
 859                                .child(Label::new("Cancel")),
 860                        ),
 861                )
 862                .into_any_element(),
 863        )
 864        .entry(self.confirm_entry.clone())
 865        .entry(self.back_entry.clone())
 866        .render(window, cx)
 867        .into_any_element()
 868    }
 869
 870    fn render_querying_templates(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 871        Navigable::new(
 872            div()
 873                .child(
 874                    div().track_focus(&self.focus_handle).child(
 875                        ModalHeader::new().child(
 876                            Headline::new("Create Dev Container").size(HeadlineSize::XSmall),
 877                        ),
 878                    ),
 879                )
 880                .child(ListSeparator)
 881                .child(
 882                    div().child(
 883                        ListItem::new("li-querying")
 884                            .inset(true)
 885                            .spacing(ui::ListItemSpacing::Sparse)
 886                            .start_slot(
 887                                Icon::new(IconName::ArrowCircle)
 888                                    .color(Color::Muted)
 889                                    .with_rotate_animation(2),
 890                            )
 891                            .child(Label::new("Querying template registry...")),
 892                    ),
 893                )
 894                .child(ListSeparator)
 895                .child(
 896                    div()
 897                        .track_focus(&self.back_entry.focus_handle)
 898                        .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
 899                            this.accept_message(DevContainerMessage::GoBack, window, cx);
 900                        }))
 901                        .child(
 902                            ListItem::new("li-goback")
 903                                .inset(true)
 904                                .spacing(ui::ListItemSpacing::Sparse)
 905                                .start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
 906                                .toggle_state(
 907                                    self.back_entry.focus_handle.contains_focused(window, cx),
 908                                )
 909                                .on_click(cx.listener(|this, _, window, cx| {
 910                                    this.accept_message(DevContainerMessage::GoBack, window, cx);
 911                                    cx.notify();
 912                                }))
 913                                .child(Label::new("Go Back")),
 914                        ),
 915                )
 916                .into_any_element(),
 917        )
 918        .entry(self.back_entry.clone())
 919        .render(window, cx)
 920        .into_any_element()
 921    }
 922    fn render_querying_features(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 923        Navigable::new(
 924            div()
 925                .child(
 926                    div().track_focus(&self.focus_handle).child(
 927                        ModalHeader::new().child(
 928                            Headline::new("Create Dev Container").size(HeadlineSize::XSmall),
 929                        ),
 930                    ),
 931                )
 932                .child(ListSeparator)
 933                .child(
 934                    div().child(
 935                        ListItem::new("li-querying")
 936                            .inset(true)
 937                            .spacing(ui::ListItemSpacing::Sparse)
 938                            .start_slot(
 939                                Icon::new(IconName::ArrowCircle)
 940                                    .color(Color::Muted)
 941                                    .with_rotate_animation(2),
 942                            )
 943                            .child(Label::new("Querying features...")),
 944                    ),
 945                )
 946                .child(ListSeparator)
 947                .child(
 948                    div()
 949                        .track_focus(&self.back_entry.focus_handle)
 950                        .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
 951                            this.accept_message(DevContainerMessage::GoBack, window, cx);
 952                        }))
 953                        .child(
 954                            ListItem::new("li-goback")
 955                                .inset(true)
 956                                .spacing(ui::ListItemSpacing::Sparse)
 957                                .start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
 958                                .toggle_state(
 959                                    self.back_entry.focus_handle.contains_focused(window, cx),
 960                                )
 961                                .on_click(cx.listener(|this, _, window, cx| {
 962                                    this.accept_message(DevContainerMessage::GoBack, window, cx);
 963                                    cx.notify();
 964                                }))
 965                                .child(Label::new("Go Back")),
 966                        ),
 967                )
 968                .into_any_element(),
 969        )
 970        .entry(self.back_entry.clone())
 971        .render(window, cx)
 972        .into_any_element()
 973    }
 974}
 975
 976impl StatefulModal for DevContainerModal {
 977    type State = DevContainerState;
 978    type Message = DevContainerMessage;
 979
 980    fn state(&self) -> Self::State {
 981        self.state.clone()
 982    }
 983
 984    fn render_for_state(
 985        &self,
 986        state: Self::State,
 987        window: &mut Window,
 988        cx: &mut Context<Self>,
 989    ) -> AnyElement {
 990        match state {
 991            DevContainerState::Initial => self.render_initial(window, cx),
 992            DevContainerState::QueryingTemplates => self.render_querying_templates(window, cx),
 993            DevContainerState::TemplateQueryReturned(Ok(_)) => {
 994                self.render_retrieved_templates(window, cx)
 995            }
 996            DevContainerState::UserOptionsSpecifying(template_entry) => {
 997                self.render_user_options_specifying(template_entry, window, cx)
 998            }
 999            DevContainerState::QueryingFeatures(_) => self.render_querying_features(window, cx),
1000            DevContainerState::FeaturesQueryReturned(_) => {
1001                self.render_features_query_returned(window, cx)
1002            }
1003            DevContainerState::ConfirmingWriteDevContainer(template_entry) => {
1004                self.render_confirming_write_dev_container(template_entry, window, cx)
1005            }
1006            DevContainerState::TemplateWriteFailed(dev_container_error) => self.render_error(
1007                "Error Creating Dev Container Definition".to_string(),
1008                dev_container_error,
1009                window,
1010                cx,
1011            ),
1012            DevContainerState::TemplateQueryReturned(Err(e)) => {
1013                self.render_error("Error Retrieving Templates".to_string(), e, window, cx)
1014            }
1015        }
1016    }
1017
1018    fn accept_message(
1019        &mut self,
1020        message: Self::Message,
1021        window: &mut Window,
1022        cx: &mut Context<Self>,
1023    ) {
1024        let new_state = match message {
1025            DevContainerMessage::SearchTemplates => {
1026                cx.spawn_in(window, async move |this, cx| {
1027                    let Ok(client) = cx.update(|_, cx| cx.http_client()) else {
1028                        return;
1029                    };
1030                    match get_templates(client).await {
1031                        Ok(templates) => {
1032                            let message =
1033                                DevContainerMessage::TemplatesRetrieved(templates.templates);
1034                            this.update_in(cx, |this, window, cx| {
1035                                this.accept_message(message, window, cx);
1036                            })
1037                            .ok();
1038                        }
1039                        Err(e) => {
1040                            let message = DevContainerMessage::ErrorRetrievingTemplates(e);
1041                            this.update_in(cx, |this, window, cx| {
1042                                this.accept_message(message, window, cx);
1043                            })
1044                            .ok();
1045                        }
1046                    }
1047                })
1048                .detach();
1049                Some(DevContainerState::QueryingTemplates)
1050            }
1051            DevContainerMessage::ErrorRetrievingTemplates(message) => {
1052                Some(DevContainerState::TemplateQueryReturned(Err(message)))
1053            }
1054            DevContainerMessage::GoBack => match &self.state {
1055                DevContainerState::Initial => Some(DevContainerState::Initial),
1056                DevContainerState::QueryingTemplates => Some(DevContainerState::Initial),
1057                DevContainerState::UserOptionsSpecifying(template_entry) => {
1058                    if template_entry.current_option_index <= 1 {
1059                        self.accept_message(DevContainerMessage::SearchTemplates, window, cx);
1060                    } else {
1061                        let mut template_entry = template_entry.clone();
1062                        template_entry.current_option_index =
1063                            template_entry.current_option_index.saturating_sub(2);
1064                        self.accept_message(
1065                            DevContainerMessage::TemplateOptionsSpecified(template_entry),
1066                            window,
1067                            cx,
1068                        );
1069                    }
1070                    None
1071                }
1072                _ => Some(DevContainerState::Initial),
1073            },
1074            DevContainerMessage::TemplatesRetrieved(items) => {
1075                let items = items
1076                    .into_iter()
1077                    .map(|item| TemplateEntry {
1078                        template: item,
1079                        options_selected: HashMap::new(),
1080                        current_option_index: 0,
1081                        current_option: None,
1082                        features_selected: HashSet::new(),
1083                    })
1084                    .collect::<Vec<TemplateEntry>>();
1085                if self.state == DevContainerState::QueryingTemplates {
1086                    let delegate = TemplatePickerDelegate::new(
1087                        "Select a template".to_string(),
1088                        cx.weak_entity(),
1089                        items.clone(),
1090                        Box::new(|entry, this, window, cx| {
1091                            this.accept_message(
1092                                DevContainerMessage::TemplateSelected(entry),
1093                                window,
1094                                cx,
1095                            );
1096                        }),
1097                    );
1098
1099                    let picker =
1100                        cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
1101                    self.picker = Some(picker);
1102                    Some(DevContainerState::TemplateQueryReturned(Ok(items)))
1103                } else {
1104                    None
1105                }
1106            }
1107            DevContainerMessage::TemplateSelected(mut template_entry) => {
1108                let Some(options) = template_entry.template.clone().options else {
1109                    return self.accept_message(
1110                        DevContainerMessage::TemplateOptionsCompleted(template_entry),
1111                        window,
1112                        cx,
1113                    );
1114                };
1115
1116                let options = options
1117                    .iter()
1118                    .collect::<Vec<(&String, &TemplateOptions)>>()
1119                    .clone();
1120
1121                let Some((first_option_name, first_option)) =
1122                    options.get(template_entry.current_option_index)
1123                else {
1124                    return self.accept_message(
1125                        DevContainerMessage::TemplateOptionsCompleted(template_entry),
1126                        window,
1127                        cx,
1128                    );
1129                };
1130
1131                let next_option_entries = first_option
1132                    .possible_values()
1133                    .into_iter()
1134                    .map(|option| (option, NavigableEntry::focusable(cx)))
1135                    .collect();
1136
1137                template_entry.current_option_index += 1;
1138                template_entry.current_option = Some(TemplateOptionSelection {
1139                    option_name: (*first_option_name).clone(),
1140                    description: first_option
1141                        .description
1142                        .clone()
1143                        .unwrap_or_else(|| "".to_string()),
1144                    navigable_options: next_option_entries,
1145                });
1146
1147                Some(DevContainerState::UserOptionsSpecifying(template_entry))
1148            }
1149            DevContainerMessage::TemplateOptionsSpecified(mut template_entry) => {
1150                let Some(options) = template_entry.template.clone().options else {
1151                    return self.accept_message(
1152                        DevContainerMessage::TemplateOptionsCompleted(template_entry),
1153                        window,
1154                        cx,
1155                    );
1156                };
1157
1158                let options = options
1159                    .iter()
1160                    .collect::<Vec<(&String, &TemplateOptions)>>()
1161                    .clone();
1162
1163                let Some((next_option_name, next_option)) =
1164                    options.get(template_entry.current_option_index)
1165                else {
1166                    return self.accept_message(
1167                        DevContainerMessage::TemplateOptionsCompleted(template_entry),
1168                        window,
1169                        cx,
1170                    );
1171                };
1172
1173                let next_option_entries = next_option
1174                    .possible_values()
1175                    .into_iter()
1176                    .map(|option| (option, NavigableEntry::focusable(cx)))
1177                    .collect();
1178
1179                template_entry.current_option_index += 1;
1180                template_entry.current_option = Some(TemplateOptionSelection {
1181                    option_name: (*next_option_name).clone(),
1182                    description: next_option
1183                        .description
1184                        .clone()
1185                        .unwrap_or_else(|| "".to_string()),
1186                    navigable_options: next_option_entries,
1187                });
1188
1189                Some(DevContainerState::UserOptionsSpecifying(template_entry))
1190            }
1191            DevContainerMessage::TemplateOptionsCompleted(template_entry) => {
1192                cx.spawn_in(window, async move |this, cx| {
1193                    let Ok(client) = cx.update(|_, cx| cx.http_client()) else {
1194                        return;
1195                    };
1196                    let Some(features) = get_features(client).await.log_err() else {
1197                        return;
1198                    };
1199                    let message = DevContainerMessage::FeaturesRetrieved(features.features);
1200                    this.update_in(cx, |this, window, cx| {
1201                        this.accept_message(message, window, cx);
1202                    })
1203                    .ok();
1204                })
1205                .detach();
1206                Some(DevContainerState::QueryingFeatures(template_entry))
1207            }
1208            DevContainerMessage::FeaturesRetrieved(features) => {
1209                if let DevContainerState::QueryingFeatures(template_entry) = self.state.clone() {
1210                    let features = features
1211                        .iter()
1212                        .map(|feature| FeatureEntry {
1213                            feature: feature.clone(),
1214                            toggle_state: ToggleState::Unselected,
1215                        })
1216                        .collect::<Vec<FeatureEntry>>();
1217                    let delegate = FeaturePickerDelegate::new(
1218                        "Select features to add".to_string(),
1219                        cx.weak_entity(),
1220                        features,
1221                        template_entry.clone(),
1222                        Box::new(|entry, this, window, cx| {
1223                            this.accept_message(
1224                                DevContainerMessage::FeaturesSelected(entry),
1225                                window,
1226                                cx,
1227                            );
1228                        }),
1229                    );
1230
1231                    let picker =
1232                        cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
1233                    self.features_picker = Some(picker);
1234                    Some(DevContainerState::FeaturesQueryReturned(template_entry))
1235                } else {
1236                    None
1237                }
1238            }
1239            DevContainerMessage::FeaturesSelected(template_entry) => {
1240                if let Some(workspace) = self.workspace.upgrade() {
1241                    dispatch_apply_templates(template_entry, workspace, window, true, cx);
1242                }
1243
1244                None
1245            }
1246            DevContainerMessage::NeedConfirmWriteDevContainer(template_entry) => Some(
1247                DevContainerState::ConfirmingWriteDevContainer(template_entry),
1248            ),
1249            DevContainerMessage::ConfirmWriteDevContainer(template_entry) => {
1250                if let Some(workspace) = self.workspace.upgrade() {
1251                    dispatch_apply_templates(template_entry, workspace, window, false, cx);
1252                }
1253                None
1254            }
1255            DevContainerMessage::FailedToWriteTemplate(error) => {
1256                Some(DevContainerState::TemplateWriteFailed(error))
1257            }
1258        };
1259        if let Some(state) = new_state {
1260            self.state = state;
1261            self.focus_handle.focus(window, cx);
1262        }
1263        cx.notify();
1264    }
1265}
1266impl EventEmitter<DismissEvent> for DevContainerModal {}
1267impl Focusable for DevContainerModal {
1268    fn focus_handle(&self, _: &App) -> FocusHandle {
1269        self.focus_handle.clone()
1270    }
1271}
1272impl ModalView for DevContainerModal {}
1273
1274impl Render for DevContainerModal {
1275    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1276        self.render_inner(window, cx)
1277    }
1278}
1279
1280trait StatefulModal: ModalView + EventEmitter<DismissEvent> + Render {
1281    type State;
1282    type Message;
1283
1284    fn state(&self) -> Self::State;
1285
1286    fn render_for_state(
1287        &self,
1288        state: Self::State,
1289        window: &mut Window,
1290        cx: &mut Context<Self>,
1291    ) -> AnyElement;
1292
1293    fn accept_message(
1294        &mut self,
1295        message: Self::Message,
1296        window: &mut Window,
1297        cx: &mut Context<Self>,
1298    );
1299
1300    fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
1301        cx.emit(DismissEvent);
1302    }
1303
1304    fn render_inner(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1305        let element = self.render_for_state(self.state(), window, cx);
1306        div()
1307            .elevation_3(cx)
1308            .w(rems(34.))
1309            .key_context("ContainerModal")
1310            .on_action(cx.listener(Self::dismiss))
1311            .child(element)
1312    }
1313}
1314
1315#[derive(Debug, Deserialize)]
1316#[serde(rename_all = "camelCase")]
1317struct GithubTokenResponse {
1318    token: String,
1319}
1320
1321fn ghcr_url() -> &'static str {
1322    "https://ghcr.io"
1323}
1324
1325fn ghcr_domain() -> &'static str {
1326    "ghcr.io"
1327}
1328
1329fn devcontainer_templates_repository() -> &'static str {
1330    "devcontainers/templates"
1331}
1332
1333fn devcontainer_features_repository() -> &'static str {
1334    "devcontainers/features"
1335}
1336
1337#[derive(Debug, Deserialize)]
1338#[serde(rename_all = "camelCase")]
1339struct ManifestLayer {
1340    digest: String,
1341}
1342#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
1343#[serde(rename_all = "camelCase")]
1344struct TemplateOptions {
1345    #[serde(rename = "type")]
1346    option_type: String,
1347    description: Option<String>,
1348    proposals: Option<Vec<String>>,
1349    #[serde(rename = "enum")]
1350    enum_values: Option<Vec<String>>,
1351    // Different repositories surface "default: 'true'" or "default: true",
1352    // so we need to be flexible in deserializing
1353    #[serde(deserialize_with = "deserialize_string_or_bool")]
1354    default: String,
1355}
1356
1357fn deserialize_string_or_bool<'de, D>(deserializer: D) -> Result<String, D::Error>
1358where
1359    D: serde::Deserializer<'de>,
1360{
1361    use serde::Deserialize;
1362
1363    #[derive(Deserialize)]
1364    #[serde(untagged)]
1365    enum StringOrBool {
1366        String(String),
1367        Bool(bool),
1368    }
1369
1370    match StringOrBool::deserialize(deserializer)? {
1371        StringOrBool::String(s) => Ok(s),
1372        StringOrBool::Bool(b) => Ok(b.to_string()),
1373    }
1374}
1375
1376impl TemplateOptions {
1377    fn possible_values(&self) -> Vec<String> {
1378        match self.option_type.as_str() {
1379            "string" => self
1380                .enum_values
1381                .clone()
1382                .or(self.proposals.clone().or(Some(vec![self.default.clone()])))
1383                .unwrap_or_default(),
1384            // If not string, must be boolean
1385            _ => {
1386                if self.default == "true" {
1387                    vec!["true".to_string(), "false".to_string()]
1388                } else {
1389                    vec!["false".to_string(), "true".to_string()]
1390                }
1391            }
1392        }
1393    }
1394}
1395
1396#[derive(Debug, Deserialize)]
1397#[serde(rename_all = "camelCase")]
1398struct DockerManifestsResponse {
1399    layers: Vec<ManifestLayer>,
1400}
1401
1402#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Hash)]
1403#[serde(rename_all = "camelCase")]
1404struct DevContainerFeature {
1405    id: String,
1406    version: String,
1407    name: String,
1408    source_repository: Option<String>,
1409}
1410
1411impl DevContainerFeature {
1412    fn major_version(&self) -> String {
1413        let Some(mv) = self.version.get(..1) else {
1414            return "".to_string();
1415        };
1416        mv.to_string()
1417    }
1418}
1419
1420#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
1421#[serde(rename_all = "camelCase")]
1422struct DevContainerTemplate {
1423    id: String,
1424    name: String,
1425    options: Option<HashMap<String, TemplateOptions>>,
1426    source_repository: Option<String>,
1427}
1428
1429#[derive(Debug, Deserialize)]
1430#[serde(rename_all = "camelCase")]
1431struct DevContainerFeaturesResponse {
1432    features: Vec<DevContainerFeature>,
1433}
1434
1435#[derive(Debug, Deserialize)]
1436#[serde(rename_all = "camelCase")]
1437struct DevContainerTemplatesResponse {
1438    templates: Vec<DevContainerTemplate>,
1439}
1440
1441fn dispatch_apply_templates(
1442    template_entry: TemplateEntry,
1443    workspace: Entity<Workspace>,
1444    window: &mut Window,
1445    check_for_existing: bool,
1446    cx: &mut Context<DevContainerModal>,
1447) {
1448    cx.spawn_in(window, async move |this, cx| {
1449        let Some((tree_id, context)) = workspace.update(cx, |workspace, cx| {
1450            let worktree = workspace
1451                .project()
1452                .read(cx)
1453                .visible_worktrees(cx)
1454                .find_map(|tree| {
1455                    tree.read(cx)
1456                        .root_entry()?
1457                        .is_dir()
1458                        .then_some(tree.read(cx))
1459                });
1460            let tree_id = worktree.map(|w| w.id())?;
1461            let context = DevContainerContext::from_workspace(workspace, cx)?;
1462            Some((tree_id, context))
1463        }) else {
1464            return;
1465        };
1466
1467        let Ok(cli) = ensure_devcontainer_cli(&context.node_runtime).await else {
1468            this.update_in(cx, |this, window, cx| {
1469                this.accept_message(
1470                    DevContainerMessage::FailedToWriteTemplate(
1471                        DevContainerError::DevContainerCliNotAvailable,
1472                    ),
1473                    window,
1474                    cx,
1475                );
1476            })
1477            .log_err();
1478            return;
1479        };
1480
1481        {
1482            if check_for_existing
1483                && read_devcontainer_configuration(&context, &cli, None)
1484                    .await
1485                    .is_ok()
1486            {
1487                this.update_in(cx, |this, window, cx| {
1488                    this.accept_message(
1489                        DevContainerMessage::NeedConfirmWriteDevContainer(template_entry),
1490                        window,
1491                        cx,
1492                    );
1493                })
1494                .ok();
1495                return;
1496            }
1497
1498            let files = match apply_dev_container_template(
1499                &template_entry.template,
1500                &template_entry.options_selected,
1501                &template_entry.features_selected,
1502                &context,
1503                &cli,
1504            )
1505            .await
1506            {
1507                Ok(files) => files,
1508                Err(e) => {
1509                    this.update_in(cx, |this, window, cx| {
1510                        this.accept_message(
1511                            DevContainerMessage::FailedToWriteTemplate(e),
1512                            window,
1513                            cx,
1514                        );
1515                    })
1516                    .ok();
1517                    return;
1518                }
1519            };
1520
1521            if files
1522                .files
1523                .contains(&"./.devcontainer/devcontainer.json".to_string())
1524            {
1525                let Some(workspace_task) = workspace
1526                    .update_in(cx, |workspace, window, cx| {
1527                        let Ok(path) = RelPath::unix(".devcontainer/devcontainer.json") else {
1528                            return Task::ready(Err(anyhow!(
1529                                "Couldn't create path for .devcontainer/devcontainer.json"
1530                            )));
1531                        };
1532                        workspace.open_path((tree_id, path), None, true, window, cx)
1533                    })
1534                    .ok()
1535                else {
1536                    return;
1537                };
1538
1539                workspace_task.await.log_err();
1540            }
1541            this.update_in(cx, |this, window, cx| {
1542                this.dismiss(&menu::Cancel, window, cx);
1543            })
1544            .ok();
1545        }
1546    })
1547    .detach();
1548}
1549
1550async fn get_templates(
1551    client: Arc<dyn HttpClient>,
1552) -> Result<DevContainerTemplatesResponse, String> {
1553    let token = get_ghcr_token(&client).await?;
1554    let manifest = get_latest_manifest(&token.token, &client).await?;
1555
1556    let mut template_response =
1557        get_devcontainer_templates(&token.token, &manifest.layers[0].digest, &client).await?;
1558
1559    for template in &mut template_response.templates {
1560        template.source_repository = Some(format!(
1561            "{}/{}",
1562            ghcr_domain(),
1563            devcontainer_templates_repository()
1564        ));
1565    }
1566    Ok(template_response)
1567}
1568
1569async fn get_features(client: Arc<dyn HttpClient>) -> Result<DevContainerFeaturesResponse, String> {
1570    let token = get_ghcr_token(&client).await?;
1571    let manifest = get_latest_feature_manifest(&token.token, &client).await?;
1572
1573    let mut features_response =
1574        get_devcontainer_features(&token.token, &manifest.layers[0].digest, &client).await?;
1575
1576    for feature in &mut features_response.features {
1577        feature.source_repository = Some(format!(
1578            "{}/{}",
1579            ghcr_domain(),
1580            devcontainer_features_repository()
1581        ));
1582    }
1583    Ok(features_response)
1584}
1585
1586async fn get_ghcr_token(client: &Arc<dyn HttpClient>) -> Result<GithubTokenResponse, String> {
1587    let url = format!(
1588        "{}/token?service=ghcr.io&scope=repository:{}:pull",
1589        ghcr_url(),
1590        devcontainer_templates_repository()
1591    );
1592    get_deserialized_response("", &url, client).await
1593}
1594
1595async fn get_latest_feature_manifest(
1596    token: &str,
1597    client: &Arc<dyn HttpClient>,
1598) -> Result<DockerManifestsResponse, String> {
1599    let url = format!(
1600        "{}/v2/{}/manifests/latest",
1601        ghcr_url(),
1602        devcontainer_features_repository()
1603    );
1604    get_deserialized_response(token, &url, client).await
1605}
1606
1607async fn get_latest_manifest(
1608    token: &str,
1609    client: &Arc<dyn HttpClient>,
1610) -> Result<DockerManifestsResponse, String> {
1611    let url = format!(
1612        "{}/v2/{}/manifests/latest",
1613        ghcr_url(),
1614        devcontainer_templates_repository()
1615    );
1616    get_deserialized_response(token, &url, client).await
1617}
1618
1619async fn get_devcontainer_features(
1620    token: &str,
1621    blob_digest: &str,
1622    client: &Arc<dyn HttpClient>,
1623) -> Result<DevContainerFeaturesResponse, String> {
1624    let url = format!(
1625        "{}/v2/{}/blobs/{}",
1626        ghcr_url(),
1627        devcontainer_features_repository(),
1628        blob_digest
1629    );
1630    get_deserialized_response(token, &url, client).await
1631}
1632
1633async fn get_devcontainer_templates(
1634    token: &str,
1635    blob_digest: &str,
1636    client: &Arc<dyn HttpClient>,
1637) -> Result<DevContainerTemplatesResponse, String> {
1638    let url = format!(
1639        "{}/v2/{}/blobs/{}",
1640        ghcr_url(),
1641        devcontainer_templates_repository(),
1642        blob_digest
1643    );
1644    get_deserialized_response(token, &url, client).await
1645}
1646
1647async fn get_deserialized_response<T>(
1648    token: &str,
1649    url: &str,
1650    client: &Arc<dyn HttpClient>,
1651) -> Result<T, String>
1652where
1653    T: for<'de> Deserialize<'de>,
1654{
1655    let request = match Request::get(url)
1656        .header("Authorization", format!("Bearer {}", token))
1657        .header("Accept", "application/vnd.oci.image.manifest.v1+json")
1658        .body(AsyncBody::default())
1659    {
1660        Ok(request) => request,
1661        Err(e) => return Err(format!("Failed to create request: {}", e)),
1662    };
1663    let response = match client.send(request).await {
1664        Ok(response) => response,
1665        Err(e) => {
1666            return Err(format!("Failed to send request: {}", e));
1667        }
1668    };
1669
1670    let mut output = String::new();
1671
1672    if let Err(e) = response.into_body().read_to_string(&mut output).await {
1673        return Err(format!("Failed to read response body: {}", e));
1674    };
1675
1676    match serde_json::from_str(&output) {
1677        Ok(response) => Ok(response),
1678        Err(e) => Err(format!("Failed to deserialize response: {}", e)),
1679    }
1680}
1681
1682#[cfg(test)]
1683mod tests {
1684    use gpui::TestAppContext;
1685    use http_client::{FakeHttpClient, anyhow};
1686
1687    use crate::{
1688        GithubTokenResponse, devcontainer_templates_repository, get_deserialized_response,
1689        get_devcontainer_templates, get_ghcr_token, get_latest_manifest,
1690    };
1691
1692    #[gpui::test]
1693    async fn test_get_deserialized_response(_cx: &mut TestAppContext) {
1694        let client = FakeHttpClient::create(|_request| async move {
1695            Ok(http_client::Response::builder()
1696                .status(200)
1697                .body("{ \"token\": \"thisisatoken\" }".into())
1698                .unwrap())
1699        });
1700
1701        let response =
1702            get_deserialized_response::<GithubTokenResponse>("", "https://ghcr.io/token", &client)
1703                .await;
1704        assert!(response.is_ok());
1705        assert_eq!(response.unwrap().token, "thisisatoken".to_string())
1706    }
1707
1708    #[gpui::test]
1709    async fn test_get_ghcr_token() {
1710        let client = FakeHttpClient::create(|request| async move {
1711            let host = request.uri().host();
1712            if host.is_none() || host.unwrap() != "ghcr.io" {
1713                return Err(anyhow!("Unexpected host: {}", host.unwrap_or_default()));
1714            }
1715            let path = request.uri().path();
1716            if path != "/token" {
1717                return Err(anyhow!("Unexpected path: {}", path));
1718            }
1719            let query = request.uri().query();
1720            if query.is_none()
1721                || query.unwrap()
1722                    != format!(
1723                        "service=ghcr.io&scope=repository:{}:pull",
1724                        devcontainer_templates_repository()
1725                    )
1726            {
1727                return Err(anyhow!("Unexpected query: {}", query.unwrap_or_default()));
1728            }
1729            Ok(http_client::Response::builder()
1730                .status(200)
1731                .body("{ \"token\": \"thisisatoken\" }".into())
1732                .unwrap())
1733        });
1734
1735        let response = get_ghcr_token(&client).await;
1736        assert!(response.is_ok());
1737        assert_eq!(response.unwrap().token, "thisisatoken".to_string());
1738    }
1739
1740    #[gpui::test]
1741    async fn test_get_latest_manifests() {
1742        let client = FakeHttpClient::create(|request| async move {
1743            let host = request.uri().host();
1744            if host.is_none() || host.unwrap() != "ghcr.io" {
1745                return Err(anyhow!("Unexpected host: {}", host.unwrap_or_default()));
1746            }
1747            let path = request.uri().path();
1748            if path
1749                != format!(
1750                    "/v2/{}/manifests/latest",
1751                    devcontainer_templates_repository()
1752                )
1753            {
1754                return Err(anyhow!("Unexpected path: {}", path));
1755            }
1756            Ok(http_client::Response::builder()
1757                .status(200)
1758                .body("{
1759                    \"schemaVersion\": 2,
1760                    \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",
1761                    \"config\": {
1762                        \"mediaType\": \"application/vnd.devcontainers\",
1763                        \"digest\": \"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\",
1764                        \"size\": 2
1765                    },
1766                    \"layers\": [
1767                        {
1768                            \"mediaType\": \"application/vnd.devcontainers.collection.layer.v1+json\",
1769                            \"digest\": \"sha256:035e9c9fd9bd61f6d3965fa4bf11f3ddfd2490a8cf324f152c13cc3724d67d09\",
1770                            \"size\": 65235,
1771                            \"annotations\": {
1772                                \"org.opencontainers.image.title\": \"devcontainer-collection.json\"
1773                            }
1774                        }
1775                    ],
1776                    \"annotations\": {
1777                        \"com.github.package.type\": \"devcontainer_collection\"
1778                    }
1779                }".into())
1780                .unwrap())
1781        });
1782
1783        let response = get_latest_manifest("", &client).await;
1784        assert!(response.is_ok());
1785        let response = response.unwrap();
1786
1787        assert_eq!(response.layers.len(), 1);
1788        assert_eq!(
1789            response.layers[0].digest,
1790            "sha256:035e9c9fd9bd61f6d3965fa4bf11f3ddfd2490a8cf324f152c13cc3724d67d09"
1791        );
1792    }
1793
1794    #[gpui::test]
1795    async fn test_get_devcontainer_templates() {
1796        let client = FakeHttpClient::create(|request| async move {
1797            let host = request.uri().host();
1798            if host.is_none() || host.unwrap() != "ghcr.io" {
1799                return Err(anyhow!("Unexpected host: {}", host.unwrap_or_default()));
1800            }
1801            let path = request.uri().path();
1802            if path
1803                != format!(
1804                    "/v2/{}/blobs/sha256:035e9c9fd9bd61f6d3965fa4bf11f3ddfd2490a8cf324f152c13cc3724d67d09",
1805                    devcontainer_templates_repository()
1806                )
1807            {
1808                return Err(anyhow!("Unexpected path: {}", path));
1809            }
1810            Ok(http_client::Response::builder()
1811                .status(200)
1812                .body("{
1813                    \"sourceInformation\": {
1814                        \"source\": \"devcontainer-cli\"
1815                    },
1816                    \"templates\": [
1817                        {
1818                            \"id\": \"alpine\",
1819                            \"version\": \"3.4.0\",
1820                            \"name\": \"Alpine\",
1821                            \"description\": \"Simple Alpine container with Git installed.\",
1822                            \"documentationURL\": \"https://github.com/devcontainers/templates/tree/main/src/alpine\",
1823                            \"publisher\": \"Dev Container Spec Maintainers\",
1824                            \"licenseURL\": \"https://github.com/devcontainers/templates/blob/main/LICENSE\",
1825                            \"options\": {
1826                                \"imageVariant\": {
1827                                    \"type\": \"string\",
1828                                    \"description\": \"Alpine version:\",
1829                                    \"proposals\": [
1830                                        \"3.21\",
1831                                        \"3.20\",
1832                                        \"3.19\",
1833                                        \"3.18\"
1834                                    ],
1835                                    \"default\": \"3.20\"
1836                                }
1837                            },
1838                            \"platforms\": [
1839                                \"Any\"
1840                            ],
1841                            \"optionalPaths\": [
1842                                \".github/dependabot.yml\"
1843                            ],
1844                            \"type\": \"image\",
1845                            \"files\": [
1846                                \"NOTES.md\",
1847                                \"README.md\",
1848                                \"devcontainer-template.json\",
1849                                \".devcontainer/devcontainer.json\",
1850                                \".github/dependabot.yml\"
1851                            ],
1852                            \"fileCount\": 5,
1853                            \"featureIds\": []
1854                        }
1855                    ]
1856                }".into())
1857                .unwrap())
1858        });
1859        let response = get_devcontainer_templates(
1860            "",
1861            "sha256:035e9c9fd9bd61f6d3965fa4bf11f3ddfd2490a8cf324f152c13cc3724d67d09",
1862            &client,
1863        )
1864        .await;
1865        assert!(response.is_ok());
1866        let response = response.unwrap();
1867        assert_eq!(response.templates.len(), 1);
1868        assert_eq!(response.templates[0].name, "Alpine");
1869    }
1870}