extensions_ui.rs

   1mod components;
   2mod extension_suggest;
   3mod extension_version_selector;
   4
   5use std::sync::OnceLock;
   6use std::time::Duration;
   7use std::{ops::Range, sync::Arc};
   8
   9use client::{ExtensionMetadata, ExtensionProvides};
  10use collections::{BTreeMap, BTreeSet};
  11use editor::{Editor, EditorElement, EditorStyle};
  12use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
  13use fuzzy::{StringMatchCandidate, match_strings};
  14use gpui::{
  15    Action, App, ClipboardItem, Context, Entity, EventEmitter, Flatten, Focusable,
  16    InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
  17    UniformListScrollHandle, WeakEntity, Window, actions, point, uniform_list,
  18};
  19use num_format::{Locale, ToFormattedString};
  20use project::DirectoryLister;
  21use release_channel::ReleaseChannel;
  22use settings::Settings;
  23use strum::IntoEnumIterator as _;
  24use theme::ThemeSettings;
  25use ui::{
  26    CheckboxWithLabel, ContextMenu, PopoverMenu, ScrollableHandle, Scrollbar, ScrollbarState,
  27    ToggleButton, Tooltip, prelude::*,
  28};
  29use vim_mode_setting::VimModeSetting;
  30use workspace::{
  31    Workspace, WorkspaceId,
  32    item::{Item, ItemEvent},
  33};
  34use zed_actions::ExtensionCategoryFilter;
  35
  36use crate::components::{ExtensionCard, FeatureUpsell};
  37use crate::extension_version_selector::{
  38    ExtensionVersionSelector, ExtensionVersionSelectorDelegate,
  39};
  40
  41actions!(zed, [InstallDevExtension]);
  42
  43pub fn init(cx: &mut App) {
  44    cx.observe_new(move |workspace: &mut Workspace, window, cx| {
  45        let Some(window) = window else {
  46            return;
  47        };
  48        workspace
  49            .register_action(
  50                move |workspace, action: &zed_actions::Extensions, window, cx| {
  51                    let provides_filter = action.category_filter.map(|category| match category {
  52                        ExtensionCategoryFilter::Themes => ExtensionProvides::Themes,
  53                        ExtensionCategoryFilter::IconThemes => ExtensionProvides::IconThemes,
  54                        ExtensionCategoryFilter::Languages => ExtensionProvides::Languages,
  55                        ExtensionCategoryFilter::Grammars => ExtensionProvides::Grammars,
  56                        ExtensionCategoryFilter::LanguageServers => {
  57                            ExtensionProvides::LanguageServers
  58                        }
  59                        ExtensionCategoryFilter::ContextServers => {
  60                            ExtensionProvides::ContextServers
  61                        }
  62                        ExtensionCategoryFilter::SlashCommands => ExtensionProvides::SlashCommands,
  63                        ExtensionCategoryFilter::IndexedDocsProviders => {
  64                            ExtensionProvides::IndexedDocsProviders
  65                        }
  66                        ExtensionCategoryFilter::Snippets => ExtensionProvides::Snippets,
  67                    });
  68
  69                    let existing = workspace
  70                        .active_pane()
  71                        .read(cx)
  72                        .items()
  73                        .find_map(|item| item.downcast::<ExtensionsPage>());
  74
  75                    if let Some(existing) = existing {
  76                        if provides_filter.is_some() {
  77                            existing.update(cx, |extensions_page, cx| {
  78                                extensions_page.change_provides_filter(provides_filter, cx);
  79                            });
  80                        }
  81
  82                        workspace.activate_item(&existing, true, true, window, cx);
  83                    } else {
  84                        let extensions_page =
  85                            ExtensionsPage::new(workspace, provides_filter, window, cx);
  86                        workspace.add_item_to_active_pane(
  87                            Box::new(extensions_page),
  88                            None,
  89                            true,
  90                            window,
  91                            cx,
  92                        )
  93                    }
  94                },
  95            )
  96            .register_action(move |workspace, _: &InstallDevExtension, window, cx| {
  97                let store = ExtensionStore::global(cx);
  98                let prompt = workspace.prompt_for_open_path(
  99                    gpui::PathPromptOptions {
 100                        files: false,
 101                        directories: true,
 102                        multiple: false,
 103                    },
 104                    DirectoryLister::Local(workspace.app_state().fs.clone()),
 105                    window,
 106                    cx,
 107                );
 108
 109                let workspace_handle = cx.entity().downgrade();
 110                window
 111                    .spawn(cx, async move |cx| {
 112                        let extension_path =
 113                            match Flatten::flatten(prompt.await.map_err(|e| e.into())) {
 114                                Ok(Some(mut paths)) => paths.pop()?,
 115                                Ok(None) => return None,
 116                                Err(err) => {
 117                                    workspace_handle
 118                                        .update(cx, |workspace, cx| {
 119                                            workspace.show_portal_error(err.to_string(), cx);
 120                                        })
 121                                        .ok();
 122                                    return None;
 123                                }
 124                            };
 125
 126                        let install_task = store
 127                            .update(cx, |store, cx| {
 128                                store.install_dev_extension(extension_path, cx)
 129                            })
 130                            .ok()?;
 131
 132                        match install_task.await {
 133                            Ok(_) => {}
 134                            Err(err) => {
 135                                workspace_handle
 136                                    .update(cx, |workspace, cx| {
 137                                        workspace.show_error(
 138                                            &err.context("failed to install dev extension"),
 139                                            cx,
 140                                        );
 141                                    })
 142                                    .ok();
 143                            }
 144                        }
 145
 146                        Some(())
 147                    })
 148                    .detach();
 149            });
 150
 151        cx.subscribe_in(workspace.project(), window, |_, _, event, window, cx| {
 152            if let project::Event::LanguageNotFound(buffer) = event {
 153                extension_suggest::suggest(buffer.clone(), window, cx);
 154            }
 155        })
 156        .detach();
 157    })
 158    .detach();
 159}
 160
 161fn extension_provides_label(provides: ExtensionProvides) -> &'static str {
 162    match provides {
 163        ExtensionProvides::Themes => "Themes",
 164        ExtensionProvides::IconThemes => "Icon Themes",
 165        ExtensionProvides::Languages => "Languages",
 166        ExtensionProvides::Grammars => "Grammars",
 167        ExtensionProvides::LanguageServers => "Language Servers",
 168        ExtensionProvides::ContextServers => "MCP Servers",
 169        ExtensionProvides::SlashCommands => "Slash Commands",
 170        ExtensionProvides::IndexedDocsProviders => "Indexed Docs Providers",
 171        ExtensionProvides::Snippets => "Snippets",
 172    }
 173}
 174
 175#[derive(Clone)]
 176pub enum ExtensionStatus {
 177    NotInstalled,
 178    Installing,
 179    Upgrading,
 180    Installed(Arc<str>),
 181    Removing,
 182}
 183
 184#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
 185enum ExtensionFilter {
 186    All,
 187    Installed,
 188    NotInstalled,
 189}
 190
 191impl ExtensionFilter {
 192    pub fn include_dev_extensions(&self) -> bool {
 193        match self {
 194            Self::All | Self::Installed => true,
 195            Self::NotInstalled => false,
 196        }
 197    }
 198}
 199
 200#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
 201enum Feature {
 202    Git,
 203    OpenIn,
 204    Vim,
 205    LanguageBash,
 206    LanguageC,
 207    LanguageCpp,
 208    LanguageGo,
 209    LanguagePython,
 210    LanguageReact,
 211    LanguageRust,
 212    LanguageTypescript,
 213}
 214
 215fn keywords_by_feature() -> &'static BTreeMap<Feature, Vec<&'static str>> {
 216    static KEYWORDS_BY_FEATURE: OnceLock<BTreeMap<Feature, Vec<&'static str>>> = OnceLock::new();
 217    KEYWORDS_BY_FEATURE.get_or_init(|| {
 218        BTreeMap::from_iter([
 219            (Feature::Git, vec!["git"]),
 220            (
 221                Feature::OpenIn,
 222                vec![
 223                    "github",
 224                    "gitlab",
 225                    "bitbucket",
 226                    "codeberg",
 227                    "sourcehut",
 228                    "permalink",
 229                    "link",
 230                    "open in",
 231                ],
 232            ),
 233            (Feature::Vim, vec!["vim"]),
 234            (Feature::LanguageBash, vec!["sh", "bash"]),
 235            (Feature::LanguageC, vec!["c", "clang"]),
 236            (Feature::LanguageCpp, vec!["c++", "cpp", "clang"]),
 237            (Feature::LanguageGo, vec!["go", "golang"]),
 238            (Feature::LanguagePython, vec!["python", "py"]),
 239            (Feature::LanguageReact, vec!["react"]),
 240            (Feature::LanguageRust, vec!["rust", "rs"]),
 241            (
 242                Feature::LanguageTypescript,
 243                vec!["type", "typescript", "ts"],
 244            ),
 245        ])
 246    })
 247}
 248
 249struct ExtensionCardButtons {
 250    install_or_uninstall: Button,
 251    upgrade: Option<Button>,
 252    configure: Option<Button>,
 253}
 254
 255pub struct ExtensionsPage {
 256    workspace: WeakEntity<Workspace>,
 257    list: UniformListScrollHandle,
 258    is_fetching_extensions: bool,
 259    filter: ExtensionFilter,
 260    remote_extension_entries: Vec<ExtensionMetadata>,
 261    dev_extension_entries: Vec<Arc<ExtensionManifest>>,
 262    filtered_remote_extension_indices: Vec<usize>,
 263    query_editor: Entity<Editor>,
 264    query_contains_error: bool,
 265    provides_filter: Option<ExtensionProvides>,
 266    _subscriptions: [gpui::Subscription; 2],
 267    extension_fetch_task: Option<Task<()>>,
 268    upsells: BTreeSet<Feature>,
 269    scrollbar_state: ScrollbarState,
 270}
 271
 272impl ExtensionsPage {
 273    pub fn new(
 274        workspace: &Workspace,
 275        provides_filter: Option<ExtensionProvides>,
 276        window: &mut Window,
 277        cx: &mut Context<Workspace>,
 278    ) -> Entity<Self> {
 279        cx.new(|cx| {
 280            let store = ExtensionStore::global(cx);
 281            let workspace_handle = workspace.weak_handle();
 282            let subscriptions = [
 283                cx.observe(&store, |_: &mut Self, _, cx| cx.notify()),
 284                cx.subscribe_in(
 285                    &store,
 286                    window,
 287                    move |this, _, event, window, cx| match event {
 288                        extension_host::Event::ExtensionsUpdated => {
 289                            this.fetch_extensions_debounced(None, cx)
 290                        }
 291                        extension_host::Event::ExtensionInstalled(extension_id) => this
 292                            .on_extension_installed(
 293                                workspace_handle.clone(),
 294                                extension_id,
 295                                window,
 296                                cx,
 297                            ),
 298                        _ => {}
 299                    },
 300                ),
 301            ];
 302
 303            let query_editor = cx.new(|cx| {
 304                let mut input = Editor::single_line(window, cx);
 305                input.set_placeholder_text("Search extensions...", cx);
 306                input
 307            });
 308            cx.subscribe(&query_editor, Self::on_query_change).detach();
 309
 310            let scroll_handle = UniformListScrollHandle::new();
 311
 312            let mut this = Self {
 313                workspace: workspace.weak_handle(),
 314                list: scroll_handle.clone(),
 315                is_fetching_extensions: false,
 316                filter: ExtensionFilter::All,
 317                dev_extension_entries: Vec::new(),
 318                filtered_remote_extension_indices: Vec::new(),
 319                remote_extension_entries: Vec::new(),
 320                query_contains_error: false,
 321                provides_filter,
 322                extension_fetch_task: None,
 323                _subscriptions: subscriptions,
 324                query_editor,
 325                upsells: BTreeSet::default(),
 326                scrollbar_state: ScrollbarState::new(scroll_handle),
 327            };
 328            this.fetch_extensions(
 329                None,
 330                Some(BTreeSet::from_iter(this.provides_filter)),
 331                None,
 332                cx,
 333            );
 334            this
 335        })
 336    }
 337
 338    fn on_extension_installed(
 339        &mut self,
 340        workspace: WeakEntity<Workspace>,
 341        extension_id: &str,
 342        window: &mut Window,
 343        cx: &mut Context<Self>,
 344    ) {
 345        let extension_store = ExtensionStore::global(cx).read(cx);
 346        let themes = extension_store
 347            .extension_themes(extension_id)
 348            .map(|name| name.to_string())
 349            .collect::<Vec<_>>();
 350        if !themes.is_empty() {
 351            workspace
 352                .update(cx, |_workspace, cx| {
 353                    window.dispatch_action(
 354                        zed_actions::theme_selector::Toggle {
 355                            themes_filter: Some(themes),
 356                        }
 357                        .boxed_clone(),
 358                        cx,
 359                    );
 360                })
 361                .ok();
 362            return;
 363        }
 364
 365        let icon_themes = extension_store
 366            .extension_icon_themes(extension_id)
 367            .map(|name| name.to_string())
 368            .collect::<Vec<_>>();
 369        if !icon_themes.is_empty() {
 370            workspace
 371                .update(cx, |_workspace, cx| {
 372                    window.dispatch_action(
 373                        zed_actions::icon_theme_selector::Toggle {
 374                            themes_filter: Some(icon_themes),
 375                        }
 376                        .boxed_clone(),
 377                        cx,
 378                    );
 379                })
 380                .ok();
 381        }
 382    }
 383
 384    /// Returns whether a dev extension currently exists for the extension with the given ID.
 385    fn dev_extension_exists(extension_id: &str, cx: &mut Context<Self>) -> bool {
 386        let extension_store = ExtensionStore::global(cx).read(cx);
 387
 388        extension_store
 389            .dev_extensions()
 390            .any(|dev_extension| dev_extension.id.as_ref() == extension_id)
 391    }
 392
 393    fn extension_status(extension_id: &str, cx: &mut Context<Self>) -> ExtensionStatus {
 394        let extension_store = ExtensionStore::global(cx).read(cx);
 395
 396        match extension_store.outstanding_operations().get(extension_id) {
 397            Some(ExtensionOperation::Install) => ExtensionStatus::Installing,
 398            Some(ExtensionOperation::Remove) => ExtensionStatus::Removing,
 399            Some(ExtensionOperation::Upgrade) => ExtensionStatus::Upgrading,
 400            None => match extension_store.installed_extensions().get(extension_id) {
 401                Some(extension) => ExtensionStatus::Installed(extension.manifest.version.clone()),
 402                None => ExtensionStatus::NotInstalled,
 403            },
 404        }
 405    }
 406
 407    fn filter_extension_entries(&mut self, cx: &mut Context<Self>) {
 408        self.filtered_remote_extension_indices.clear();
 409        self.filtered_remote_extension_indices.extend(
 410            self.remote_extension_entries
 411                .iter()
 412                .enumerate()
 413                .filter(|(_, extension)| match self.filter {
 414                    ExtensionFilter::All => true,
 415                    ExtensionFilter::Installed => {
 416                        let status = Self::extension_status(&extension.id, cx);
 417                        matches!(status, ExtensionStatus::Installed(_))
 418                    }
 419                    ExtensionFilter::NotInstalled => {
 420                        let status = Self::extension_status(&extension.id, cx);
 421
 422                        matches!(status, ExtensionStatus::NotInstalled)
 423                    }
 424                })
 425                .map(|(ix, _)| ix),
 426        );
 427        cx.notify();
 428    }
 429
 430    fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
 431        self.list.set_offset(point(px(0.), px(0.)));
 432        cx.notify();
 433    }
 434
 435    fn fetch_extensions(
 436        &mut self,
 437        search: Option<String>,
 438        provides_filter: Option<BTreeSet<ExtensionProvides>>,
 439        on_complete: Option<Box<dyn FnOnce(&mut Self, &mut Context<Self>) + Send>>,
 440        cx: &mut Context<Self>,
 441    ) {
 442        self.is_fetching_extensions = true;
 443        cx.notify();
 444
 445        let extension_store = ExtensionStore::global(cx);
 446
 447        let dev_extensions = extension_store.update(cx, |store, _| {
 448            store.dev_extensions().cloned().collect::<Vec<_>>()
 449        });
 450
 451        let remote_extensions = extension_store.update(cx, |store, cx| {
 452            store.fetch_extensions(search.as_deref(), provides_filter.as_ref(), cx)
 453        });
 454
 455        cx.spawn(async move |this, cx| {
 456            let dev_extensions = if let Some(search) = search {
 457                let match_candidates = dev_extensions
 458                    .iter()
 459                    .enumerate()
 460                    .map(|(ix, manifest)| StringMatchCandidate::new(ix, &manifest.name))
 461                    .collect::<Vec<_>>();
 462
 463                let matches = match_strings(
 464                    &match_candidates,
 465                    &search,
 466                    false,
 467                    match_candidates.len(),
 468                    &Default::default(),
 469                    cx.background_executor().clone(),
 470                )
 471                .await;
 472                matches
 473                    .into_iter()
 474                    .map(|mat| dev_extensions[mat.candidate_id].clone())
 475                    .collect()
 476            } else {
 477                dev_extensions
 478            };
 479
 480            let fetch_result = remote_extensions.await;
 481            this.update(cx, |this, cx| {
 482                cx.notify();
 483                this.dev_extension_entries = dev_extensions;
 484                this.is_fetching_extensions = false;
 485                this.remote_extension_entries = fetch_result?;
 486                this.filter_extension_entries(cx);
 487                if let Some(callback) = on_complete {
 488                    callback(this, cx);
 489                }
 490                anyhow::Ok(())
 491            })?
 492        })
 493        .detach_and_log_err(cx);
 494    }
 495
 496    fn render_extensions(
 497        &mut self,
 498        range: Range<usize>,
 499        _: &mut Window,
 500        cx: &mut Context<Self>,
 501    ) -> Vec<ExtensionCard> {
 502        let dev_extension_entries_len = if self.filter.include_dev_extensions() {
 503            self.dev_extension_entries.len()
 504        } else {
 505            0
 506        };
 507        range
 508            .map(|ix| {
 509                if ix < dev_extension_entries_len {
 510                    let extension = &self.dev_extension_entries[ix];
 511                    self.render_dev_extension(extension, cx)
 512                } else {
 513                    let extension_ix =
 514                        self.filtered_remote_extension_indices[ix - dev_extension_entries_len];
 515                    let extension = &self.remote_extension_entries[extension_ix];
 516                    self.render_remote_extension(extension, cx)
 517                }
 518            })
 519            .collect()
 520    }
 521
 522    fn render_dev_extension(
 523        &self,
 524        extension: &ExtensionManifest,
 525        cx: &mut Context<Self>,
 526    ) -> ExtensionCard {
 527        let status = Self::extension_status(&extension.id, cx);
 528
 529        let repository_url = extension.repository.clone();
 530
 531        let can_configure = !extension.context_servers.is_empty();
 532
 533        ExtensionCard::new()
 534            .child(
 535                h_flex()
 536                    .justify_between()
 537                    .child(
 538                        h_flex()
 539                            .gap_2()
 540                            .items_end()
 541                            .child(Headline::new(extension.name.clone()).size(HeadlineSize::Medium))
 542                            .child(
 543                                Headline::new(format!("v{}", extension.version))
 544                                    .size(HeadlineSize::XSmall),
 545                            ),
 546                    )
 547                    .child(
 548                        h_flex()
 549                            .gap_2()
 550                            .justify_between()
 551                            .child(
 552                                Button::new(
 553                                    SharedString::from(format!("rebuild-{}", extension.id)),
 554                                    "Rebuild",
 555                                )
 556                                .on_click({
 557                                    let extension_id = extension.id.clone();
 558                                    move |_, _, cx| {
 559                                        ExtensionStore::global(cx).update(cx, |store, cx| {
 560                                            store.rebuild_dev_extension(extension_id.clone(), cx)
 561                                        });
 562                                    }
 563                                })
 564                                .color(Color::Accent)
 565                                .disabled(matches!(status, ExtensionStatus::Upgrading)),
 566                            )
 567                            .child(
 568                                Button::new(SharedString::from(extension.id.clone()), "Uninstall")
 569                                    .on_click({
 570                                        let extension_id = extension.id.clone();
 571                                        move |_, _, cx| {
 572                                            ExtensionStore::global(cx).update(cx, |store, cx| {
 573                                                store.uninstall_extension(extension_id.clone(), cx)
 574                                            });
 575                                        }
 576                                    })
 577                                    .color(Color::Accent)
 578                                    .disabled(matches!(status, ExtensionStatus::Removing)),
 579                            )
 580                            .when(can_configure, |this| {
 581                                this.child(
 582                                    Button::new(
 583                                        SharedString::from(format!("configure-{}", extension.id)),
 584                                        "Configure",
 585                                    )
 586
 587
 588                                    .on_click({
 589                                        let manifest = Arc::new(extension.clone());
 590                                        move |_, _, cx| {
 591                                            if let Some(events) =
 592                                                extension::ExtensionEvents::try_global(cx)
 593                                            {
 594                                                events.update(cx, |this, cx| {
 595                                                    this.emit(
 596                                                        extension::Event::ConfigureExtensionRequested(
 597                                                            manifest.clone(),
 598                                                        ),
 599                                                        cx,
 600                                                    )
 601                                                });
 602                                            }
 603                                        }
 604                                    })
 605                                    .color(Color::Accent)
 606                                    .disabled(matches!(status, ExtensionStatus::Installing)),
 607                                )
 608                            }),
 609                    ),
 610            )
 611            .child(
 612                h_flex()
 613                    .gap_2()
 614                    .justify_between()
 615                    .child(
 616                        Label::new(format!(
 617                            "{}: {}",
 618                            if extension.authors.len() > 1 {
 619                                "Authors"
 620                            } else {
 621                                "Author"
 622                            },
 623                            extension.authors.join(", ")
 624                        ))
 625                        .size(LabelSize::Small)
 626                        .color(Color::Muted)
 627                        .truncate(),
 628                    )
 629                    .child(Label::new("<>").size(LabelSize::Small)),
 630            )
 631            .child(
 632                h_flex()
 633                    .gap_2()
 634                    .justify_between()
 635                    .children(extension.description.as_ref().map(|description| {
 636                        Label::new(description.clone())
 637                            .size(LabelSize::Small)
 638                            .color(Color::Default)
 639                            .truncate()
 640                    }))
 641                    .children(repository_url.map(|repository_url| {
 642                        IconButton::new(
 643                            SharedString::from(format!("repository-{}", extension.id)),
 644                            IconName::Github,
 645                        )
 646                        .icon_color(Color::Accent)
 647                        .icon_size(IconSize::Small)
 648                        .on_click(cx.listener({
 649                            let repository_url = repository_url.clone();
 650                            move |_, _, _, cx| {
 651                                cx.open_url(&repository_url);
 652                            }
 653                        }))
 654                        .tooltip(Tooltip::text(repository_url.clone()))
 655                    })),
 656            )
 657    }
 658
 659    fn render_remote_extension(
 660        &self,
 661        extension: &ExtensionMetadata,
 662        cx: &mut Context<Self>,
 663    ) -> ExtensionCard {
 664        let this = cx.entity().clone();
 665        let status = Self::extension_status(&extension.id, cx);
 666        let has_dev_extension = Self::dev_extension_exists(&extension.id, cx);
 667
 668        let extension_id = extension.id.clone();
 669        let buttons = self.buttons_for_entry(extension, &status, has_dev_extension, cx);
 670        let version = extension.manifest.version.clone();
 671        let repository_url = extension.manifest.repository.clone();
 672        let authors = extension.manifest.authors.clone();
 673
 674        let installed_version = match status {
 675            ExtensionStatus::Installed(installed_version) => Some(installed_version),
 676            _ => None,
 677        };
 678
 679        ExtensionCard::new()
 680            .overridden_by_dev_extension(has_dev_extension)
 681            .child(
 682                h_flex()
 683                    .justify_between()
 684                    .child(
 685                        h_flex()
 686                            .gap_2()
 687                            .child(
 688                                Headline::new(extension.manifest.name.clone())
 689                                    .size(HeadlineSize::Medium),
 690                            )
 691                            .child(Headline::new(format!("v{version}")).size(HeadlineSize::XSmall))
 692                            .children(
 693                                installed_version
 694                                    .filter(|installed_version| *installed_version != version)
 695                                    .map(|installed_version| {
 696                                        Headline::new(format!("(v{installed_version} installed)",))
 697                                            .size(HeadlineSize::XSmall)
 698                                    }),
 699                            )
 700                            .map(|parent| {
 701                                if extension.manifest.provides.is_empty() {
 702                                    return parent;
 703                                }
 704
 705                                parent.child(
 706                                    h_flex().gap_2().children(
 707                                        extension
 708                                            .manifest
 709                                            .provides
 710                                            .iter()
 711                                            .map(|provides| {
 712                                                div()
 713                                                    .bg(cx.theme().colors().element_background)
 714                                                    .px_0p5()
 715                                                    .border_1()
 716                                                    .border_color(cx.theme().colors().border)
 717                                                    .rounded_sm()
 718                                                    .child(
 719                                                        Label::new(extension_provides_label(
 720                                                            *provides,
 721                                                        ))
 722                                                        .size(LabelSize::XSmall),
 723                                                    )
 724                                            })
 725                                            .collect::<Vec<_>>(),
 726                                    ),
 727                                )
 728                            }),
 729                    )
 730                    .child(
 731                        h_flex()
 732                            .gap_2()
 733                            .justify_between()
 734                            .children(buttons.upgrade)
 735                            .children(buttons.configure)
 736                            .child(buttons.install_or_uninstall),
 737                    ),
 738            )
 739            .child(
 740                h_flex()
 741                    .gap_2()
 742                    .justify_between()
 743                    .child(
 744                        Label::new(format!(
 745                            "{}: {}",
 746                            if extension.manifest.authors.len() > 1 {
 747                                "Authors"
 748                            } else {
 749                                "Author"
 750                            },
 751                            extension.manifest.authors.join(", ")
 752                        ))
 753                        .size(LabelSize::Small)
 754                        .color(Color::Muted)
 755                        .truncate(),
 756                    )
 757                    .child(
 758                        Label::new(format!(
 759                            "Downloads: {}",
 760                            extension.download_count.to_formatted_string(&Locale::en)
 761                        ))
 762                        .size(LabelSize::Small),
 763                    ),
 764            )
 765            .child(
 766                h_flex()
 767                    .gap_2()
 768                    .justify_between()
 769                    .children(extension.manifest.description.as_ref().map(|description| {
 770                        Label::new(description.clone())
 771                            .size(LabelSize::Small)
 772                            .color(Color::Default)
 773                            .truncate()
 774                    }))
 775                    .child(
 776                        h_flex()
 777                            .gap_2()
 778                            .child(
 779                                IconButton::new(
 780                                    SharedString::from(format!("repository-{}", extension.id)),
 781                                    IconName::Github,
 782                                )
 783                                .icon_color(Color::Accent)
 784                                .icon_size(IconSize::Small)
 785                                .on_click(cx.listener({
 786                                    let repository_url = repository_url.clone();
 787                                    move |_, _, _, cx| {
 788                                        cx.open_url(&repository_url);
 789                                    }
 790                                }))
 791                                .tooltip(Tooltip::text(repository_url.clone())),
 792                            )
 793                            .child(
 794                                PopoverMenu::new(SharedString::from(format!(
 795                                    "more-{}",
 796                                    extension.id
 797                                )))
 798                                .trigger(
 799                                    IconButton::new(
 800                                        SharedString::from(format!("more-{}", extension.id)),
 801                                        IconName::Ellipsis,
 802                                    )
 803                                    .icon_color(Color::Accent)
 804                                    .icon_size(IconSize::Small),
 805                                )
 806                                .menu(move |window, cx| {
 807                                    Some(Self::render_remote_extension_context_menu(
 808                                        &this,
 809                                        extension_id.clone(),
 810                                        authors.clone(),
 811                                        window,
 812                                        cx,
 813                                    ))
 814                                }),
 815                            ),
 816                    ),
 817            )
 818    }
 819
 820    fn render_remote_extension_context_menu(
 821        this: &Entity<Self>,
 822        extension_id: Arc<str>,
 823        authors: Vec<String>,
 824        window: &mut Window,
 825        cx: &mut App,
 826    ) -> Entity<ContextMenu> {
 827        let context_menu = ContextMenu::build(window, cx, |context_menu, window, _| {
 828            context_menu
 829                .entry(
 830                    "Install Another Version...",
 831                    None,
 832                    window.handler_for(this, {
 833                        let extension_id = extension_id.clone();
 834                        move |this, window, cx| {
 835                            this.show_extension_version_list(extension_id.clone(), window, cx)
 836                        }
 837                    }),
 838                )
 839                .entry("Copy Extension ID", None, {
 840                    let extension_id = extension_id.clone();
 841                    move |_, cx| {
 842                        cx.write_to_clipboard(ClipboardItem::new_string(extension_id.to_string()));
 843                    }
 844                })
 845                .entry("Copy Author Info", None, {
 846                    let authors = authors.clone();
 847                    move |_, cx| {
 848                        cx.write_to_clipboard(ClipboardItem::new_string(authors.join(", ")));
 849                    }
 850                })
 851        });
 852
 853        context_menu
 854    }
 855
 856    fn show_extension_version_list(
 857        &mut self,
 858        extension_id: Arc<str>,
 859        window: &mut Window,
 860        cx: &mut Context<Self>,
 861    ) {
 862        let Some(workspace) = self.workspace.upgrade() else {
 863            return;
 864        };
 865
 866        cx.spawn_in(window, async move |this, cx| {
 867            let extension_versions_task = this.update(cx, |_, cx| {
 868                let extension_store = ExtensionStore::global(cx);
 869
 870                extension_store.update(cx, |store, cx| {
 871                    store.fetch_extension_versions(&extension_id, cx)
 872                })
 873            })?;
 874
 875            let extension_versions = extension_versions_task.await?;
 876
 877            workspace.update_in(cx, |workspace, window, cx| {
 878                let fs = workspace.project().read(cx).fs().clone();
 879                workspace.toggle_modal(window, cx, |window, cx| {
 880                    let delegate = ExtensionVersionSelectorDelegate::new(
 881                        fs,
 882                        cx.entity().downgrade(),
 883                        extension_versions,
 884                    );
 885
 886                    ExtensionVersionSelector::new(delegate, window, cx)
 887                });
 888            })?;
 889
 890            anyhow::Ok(())
 891        })
 892        .detach_and_log_err(cx);
 893    }
 894
 895    fn buttons_for_entry(
 896        &self,
 897        extension: &ExtensionMetadata,
 898        status: &ExtensionStatus,
 899        has_dev_extension: bool,
 900        cx: &mut Context<Self>,
 901    ) -> ExtensionCardButtons {
 902        let is_compatible =
 903            extension_host::is_version_compatible(ReleaseChannel::global(cx), extension);
 904
 905        if has_dev_extension {
 906            // If we have a dev extension for the given extension, just treat it as uninstalled.
 907            // The button here is a placeholder, as it won't be interactable anyways.
 908            return ExtensionCardButtons {
 909                install_or_uninstall: Button::new(
 910                    SharedString::from(extension.id.clone()),
 911                    "Install",
 912                ),
 913                configure: None,
 914                upgrade: None,
 915            };
 916        }
 917
 918        let is_configurable = extension
 919            .manifest
 920            .provides
 921            .contains(&ExtensionProvides::ContextServers);
 922
 923        match status.clone() {
 924            ExtensionStatus::NotInstalled => ExtensionCardButtons {
 925                install_or_uninstall: Button::new(
 926                    SharedString::from(extension.id.clone()),
 927                    "Install",
 928                )
 929                .on_click({
 930                    let extension_id = extension.id.clone();
 931                    move |_, _, cx| {
 932                        telemetry::event!("Extension Installed");
 933                        ExtensionStore::global(cx).update(cx, |store, cx| {
 934                            store.install_latest_extension(extension_id.clone(), cx)
 935                        });
 936                    }
 937                }),
 938                configure: None,
 939                upgrade: None,
 940            },
 941            ExtensionStatus::Installing => ExtensionCardButtons {
 942                install_or_uninstall: Button::new(
 943                    SharedString::from(extension.id.clone()),
 944                    "Install",
 945                )
 946                .disabled(true),
 947                configure: None,
 948                upgrade: None,
 949            },
 950            ExtensionStatus::Upgrading => ExtensionCardButtons {
 951                install_or_uninstall: Button::new(
 952                    SharedString::from(extension.id.clone()),
 953                    "Uninstall",
 954                )
 955                .disabled(true),
 956                configure: is_configurable.then(|| {
 957                    Button::new(
 958                        SharedString::from(format!("configure-{}", extension.id.clone())),
 959                        "Configure",
 960                    )
 961                    .disabled(true)
 962                }),
 963                upgrade: Some(
 964                    Button::new(SharedString::from(extension.id.clone()), "Upgrade").disabled(true),
 965                ),
 966            },
 967            ExtensionStatus::Installed(installed_version) => ExtensionCardButtons {
 968                install_or_uninstall: Button::new(
 969                    SharedString::from(extension.id.clone()),
 970                    "Uninstall",
 971                )
 972                .on_click({
 973                    let extension_id = extension.id.clone();
 974                    move |_, _, cx| {
 975                        telemetry::event!("Extension Uninstalled", extension_id);
 976                        ExtensionStore::global(cx).update(cx, |store, cx| {
 977                            store.uninstall_extension(extension_id.clone(), cx)
 978                        });
 979                    }
 980                }),
 981                configure: is_configurable.then(|| {
 982                    Button::new(
 983                        SharedString::from(format!("configure-{}", extension.id.clone())),
 984                        "Configure",
 985                    )
 986                    .on_click({
 987                        let extension_id = extension.id.clone();
 988                        move |_, _, cx| {
 989                            if let Some(manifest) = ExtensionStore::global(cx)
 990                                .read(cx)
 991                                .extension_manifest_for_id(&extension_id)
 992                                .cloned()
 993                            {
 994                                if let Some(events) = extension::ExtensionEvents::try_global(cx) {
 995                                    events.update(cx, |this, cx| {
 996                                        this.emit(
 997                                            extension::Event::ConfigureExtensionRequested(manifest),
 998                                            cx,
 999                                        )
1000                                    });
1001                                }
1002                            }
1003                        }
1004                    })
1005                }),
1006                upgrade: if installed_version == extension.manifest.version {
1007                    None
1008                } else {
1009                    Some(
1010                        Button::new(SharedString::from(extension.id.clone()), "Upgrade")
1011                            .when(!is_compatible, |upgrade_button| {
1012                                upgrade_button.disabled(true).tooltip({
1013                                    let version = extension.manifest.version.clone();
1014                                    move |_, cx| {
1015                                        Tooltip::simple(
1016                                            format!(
1017                                                "v{version} is not compatible with this version of Zed.",
1018                                            ),
1019                                             cx,
1020                                        )
1021                                    }
1022                                })
1023                            })
1024                            .disabled(!is_compatible)
1025                            .on_click({
1026                                let extension_id = extension.id.clone();
1027                                let version = extension.manifest.version.clone();
1028                                move |_, _, cx| {
1029                                    telemetry::event!("Extension Installed", extension_id, version);
1030                                    ExtensionStore::global(cx).update(cx, |store, cx| {
1031                                        store
1032                                            .upgrade_extension(
1033                                                extension_id.clone(),
1034                                                version.clone(),
1035                                                cx,
1036                                            )
1037                                            .detach_and_log_err(cx)
1038                                    });
1039                                }
1040                            }),
1041                    )
1042                },
1043            },
1044            ExtensionStatus::Removing => ExtensionCardButtons {
1045                install_or_uninstall: Button::new(
1046                    SharedString::from(extension.id.clone()),
1047                    "Uninstall",
1048                )
1049                .disabled(true),
1050                configure: is_configurable.then(|| {
1051                    Button::new(
1052                        SharedString::from(format!("configure-{}", extension.id.clone())),
1053                        "Configure",
1054                    )
1055                    .disabled(true)
1056                }),
1057                upgrade: None,
1058            },
1059        }
1060    }
1061
1062    fn render_search(&self, cx: &mut Context<Self>) -> Div {
1063        let mut key_context = KeyContext::new_with_defaults();
1064        key_context.add("BufferSearchBar");
1065
1066        let editor_border = if self.query_contains_error {
1067            Color::Error.color(cx)
1068        } else {
1069            cx.theme().colors().border
1070        };
1071
1072        h_flex()
1073            .key_context(key_context)
1074            .h_8()
1075            .flex_1()
1076            .min_w(rems_from_px(384.))
1077            .pl_1p5()
1078            .pr_2()
1079            .py_1()
1080            .gap_2()
1081            .border_1()
1082            .border_color(editor_border)
1083            .rounded_lg()
1084            .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted))
1085            .child(self.render_text_input(&self.query_editor, cx))
1086    }
1087
1088    fn render_text_input(
1089        &self,
1090        editor: &Entity<Editor>,
1091        cx: &mut Context<Self>,
1092    ) -> impl IntoElement {
1093        let settings = ThemeSettings::get_global(cx);
1094        let text_style = TextStyle {
1095            color: if editor.read(cx).read_only(cx) {
1096                cx.theme().colors().text_disabled
1097            } else {
1098                cx.theme().colors().text
1099            },
1100            font_family: settings.ui_font.family.clone(),
1101            font_features: settings.ui_font.features.clone(),
1102            font_fallbacks: settings.ui_font.fallbacks.clone(),
1103            font_size: rems(0.875).into(),
1104            font_weight: settings.ui_font.weight,
1105            line_height: relative(1.3),
1106            ..Default::default()
1107        };
1108
1109        EditorElement::new(
1110            editor,
1111            EditorStyle {
1112                background: cx.theme().colors().editor_background,
1113                local_player: cx.theme().players().local(),
1114                text: text_style,
1115                ..Default::default()
1116            },
1117        )
1118    }
1119
1120    fn on_query_change(
1121        &mut self,
1122        _: Entity<Editor>,
1123        event: &editor::EditorEvent,
1124        cx: &mut Context<Self>,
1125    ) {
1126        if let editor::EditorEvent::Edited { .. } = event {
1127            self.query_contains_error = false;
1128            self.refresh_search(cx);
1129        }
1130    }
1131
1132    fn refresh_search(&mut self, cx: &mut Context<Self>) {
1133        self.fetch_extensions_debounced(
1134            Some(Box::new(|this, cx| {
1135                this.scroll_to_top(cx);
1136            })),
1137            cx,
1138        );
1139        self.refresh_feature_upsells(cx);
1140    }
1141
1142    pub fn change_provides_filter(
1143        &mut self,
1144        provides_filter: Option<ExtensionProvides>,
1145        cx: &mut Context<Self>,
1146    ) {
1147        self.provides_filter = provides_filter;
1148        self.refresh_search(cx);
1149    }
1150
1151    fn fetch_extensions_debounced(
1152        &mut self,
1153        on_complete: Option<Box<dyn FnOnce(&mut Self, &mut Context<Self>) + Send>>,
1154        cx: &mut Context<ExtensionsPage>,
1155    ) {
1156        self.extension_fetch_task = Some(cx.spawn(async move |this, cx| {
1157            let search = this
1158                .update(cx, |this, cx| this.search_query(cx))
1159                .ok()
1160                .flatten();
1161
1162            // Only debounce the fetching of extensions if we have a search
1163            // query.
1164            //
1165            // If the search was just cleared then we can just reload the list
1166            // of extensions without a debounce, which allows us to avoid seeing
1167            // an intermittent flash of a "no extensions" state.
1168            if search.is_some() {
1169                cx.background_executor()
1170                    .timer(Duration::from_millis(250))
1171                    .await;
1172            };
1173
1174            this.update(cx, |this, cx| {
1175                this.fetch_extensions(
1176                    search,
1177                    Some(BTreeSet::from_iter(this.provides_filter)),
1178                    on_complete,
1179                    cx,
1180                );
1181            })
1182            .ok();
1183        }));
1184    }
1185
1186    pub fn search_query(&self, cx: &mut App) -> Option<String> {
1187        let search = self.query_editor.read(cx).text(cx);
1188        if search.trim().is_empty() {
1189            None
1190        } else {
1191            Some(search)
1192        }
1193    }
1194
1195    fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
1196        let has_search = self.search_query(cx).is_some();
1197
1198        let message = if self.is_fetching_extensions {
1199            "Loading extensions..."
1200        } else {
1201            match self.filter {
1202                ExtensionFilter::All => {
1203                    if has_search {
1204                        "No extensions that match your search."
1205                    } else {
1206                        "No extensions."
1207                    }
1208                }
1209                ExtensionFilter::Installed => {
1210                    if has_search {
1211                        "No installed extensions that match your search."
1212                    } else {
1213                        "No installed extensions."
1214                    }
1215                }
1216                ExtensionFilter::NotInstalled => {
1217                    if has_search {
1218                        "No not installed extensions that match your search."
1219                    } else {
1220                        "No not installed extensions."
1221                    }
1222                }
1223            }
1224        };
1225
1226        Label::new(message)
1227    }
1228
1229    fn update_settings<T: Settings>(
1230        &mut self,
1231        selection: &ToggleState,
1232
1233        cx: &mut Context<Self>,
1234        callback: impl 'static + Send + Fn(&mut T::FileContent, bool),
1235    ) {
1236        if let Some(workspace) = self.workspace.upgrade() {
1237            let fs = workspace.read(cx).app_state().fs.clone();
1238            let selection = *selection;
1239            settings::update_settings_file::<T>(fs, cx, move |settings, _| {
1240                let value = match selection {
1241                    ToggleState::Unselected => false,
1242                    ToggleState::Selected => true,
1243                    _ => return,
1244                };
1245
1246                callback(settings, value)
1247            });
1248        }
1249    }
1250
1251    fn refresh_feature_upsells(&mut self, cx: &mut Context<Self>) {
1252        let Some(search) = self.search_query(cx) else {
1253            self.upsells.clear();
1254            return;
1255        };
1256
1257        let search = search.to_lowercase();
1258        let search_terms = search
1259            .split_whitespace()
1260            .map(|term| term.trim())
1261            .collect::<Vec<_>>();
1262
1263        for (feature, keywords) in keywords_by_feature() {
1264            if keywords
1265                .iter()
1266                .any(|keyword| search_terms.contains(keyword))
1267            {
1268                self.upsells.insert(*feature);
1269            } else {
1270                self.upsells.remove(feature);
1271            }
1272        }
1273    }
1274
1275    fn render_feature_upsells(&self, cx: &mut Context<Self>) -> impl IntoElement {
1276        let upsells_count = self.upsells.len();
1277
1278        v_flex().children(self.upsells.iter().enumerate().map(|(ix, feature)| {
1279            let upsell = match feature {
1280                Feature::Git => FeatureUpsell::new(
1281                    "Zed comes with basic Git support. More Git features are coming in the future.",
1282                )
1283                .docs_url("https://zed.dev/docs/git"),
1284                Feature::OpenIn => FeatureUpsell::new(
1285                    "Zed supports linking to a source line on GitHub and others.",
1286                )
1287                .docs_url("https://zed.dev/docs/git#git-integrations"),
1288                Feature::Vim => FeatureUpsell::new("Vim support is built-in to Zed!")
1289                    .docs_url("https://zed.dev/docs/vim")
1290                    .child(CheckboxWithLabel::new(
1291                        "enable-vim",
1292                        Label::new("Enable vim mode"),
1293                        if VimModeSetting::get_global(cx).0 {
1294                            ui::ToggleState::Selected
1295                        } else {
1296                            ui::ToggleState::Unselected
1297                        },
1298                        cx.listener(move |this, selection, _, cx| {
1299                            telemetry::event!("Vim Mode Toggled", source = "Feature Upsell");
1300                            this.update_settings::<VimModeSetting>(
1301                                selection,
1302                                cx,
1303                                |setting, value| *setting = Some(value),
1304                            );
1305                        }),
1306                    )),
1307                Feature::LanguageBash => FeatureUpsell::new("Shell support is built-in to Zed!")
1308                    .docs_url("https://zed.dev/docs/languages/bash"),
1309                Feature::LanguageC => FeatureUpsell::new("C support is built-in to Zed!")
1310                    .docs_url("https://zed.dev/docs/languages/c"),
1311                Feature::LanguageCpp => FeatureUpsell::new("C++ support is built-in to Zed!")
1312                    .docs_url("https://zed.dev/docs/languages/cpp"),
1313                Feature::LanguageGo => FeatureUpsell::new("Go support is built-in to Zed!")
1314                    .docs_url("https://zed.dev/docs/languages/go"),
1315                Feature::LanguagePython => FeatureUpsell::new("Python support is built-in to Zed!")
1316                    .docs_url("https://zed.dev/docs/languages/python"),
1317                Feature::LanguageReact => FeatureUpsell::new("React support is built-in to Zed!")
1318                    .docs_url("https://zed.dev/docs/languages/typescript"),
1319                Feature::LanguageRust => FeatureUpsell::new("Rust support is built-in to Zed!")
1320                    .docs_url("https://zed.dev/docs/languages/rust"),
1321                Feature::LanguageTypescript => {
1322                    FeatureUpsell::new("Typescript support is built-in to Zed!")
1323                        .docs_url("https://zed.dev/docs/languages/typescript")
1324                }
1325            };
1326
1327            upsell.when(ix < upsells_count, |upsell| upsell.border_b_1())
1328        }))
1329    }
1330}
1331
1332impl Render for ExtensionsPage {
1333    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1334        v_flex()
1335            .size_full()
1336            .bg(cx.theme().colors().editor_background)
1337            .child(
1338                v_flex()
1339                    .gap_4()
1340                    .pt_4()
1341                    .px_4()
1342                    .bg(cx.theme().colors().editor_background)
1343                    .child(
1344                        h_flex()
1345                            .w_full()
1346                            .gap_2()
1347                            .justify_between()
1348                            .child(Headline::new("Extensions").size(HeadlineSize::XLarge))
1349                            .child(
1350                                Button::new("install-dev-extension", "Install Dev Extension")
1351                                    .style(ButtonStyle::Filled)
1352                                    .size(ButtonSize::Large)
1353                                    .on_click(|_event, window, cx| {
1354                                        window.dispatch_action(Box::new(InstallDevExtension), cx)
1355                                    }),
1356                            ),
1357                    )
1358                    .child(
1359                        h_flex()
1360                            .w_full()
1361                            .gap_4()
1362                            .flex_wrap()
1363                            .child(self.render_search(cx))
1364                            .child(
1365                                h_flex()
1366                                    .child(
1367                                        ToggleButton::new("filter-all", "All")
1368                                            .style(ButtonStyle::Filled)
1369                                            .size(ButtonSize::Large)
1370                                            .toggle_state(self.filter == ExtensionFilter::All)
1371                                            .on_click(cx.listener(|this, _event, _, cx| {
1372                                                this.filter = ExtensionFilter::All;
1373                                                this.filter_extension_entries(cx);
1374                                                this.scroll_to_top(cx);
1375                                            }))
1376                                            .tooltip(move |_, cx| {
1377                                                Tooltip::simple("Show all extensions", cx)
1378                                            })
1379                                            .first(),
1380                                    )
1381                                    .child(
1382                                        ToggleButton::new("filter-installed", "Installed")
1383                                            .style(ButtonStyle::Filled)
1384                                            .size(ButtonSize::Large)
1385                                            .toggle_state(self.filter == ExtensionFilter::Installed)
1386                                            .on_click(cx.listener(|this, _event, _, cx| {
1387                                                this.filter = ExtensionFilter::Installed;
1388                                                this.filter_extension_entries(cx);
1389                                                this.scroll_to_top(cx);
1390                                            }))
1391                                            .tooltip(move |_, cx| {
1392                                                Tooltip::simple("Show installed extensions", cx)
1393                                            })
1394                                            .middle(),
1395                                    )
1396                                    .child(
1397                                        ToggleButton::new("filter-not-installed", "Not Installed")
1398                                            .style(ButtonStyle::Filled)
1399                                            .size(ButtonSize::Large)
1400                                            .toggle_state(
1401                                                self.filter == ExtensionFilter::NotInstalled,
1402                                            )
1403                                            .on_click(cx.listener(|this, _event, _, cx| {
1404                                                this.filter = ExtensionFilter::NotInstalled;
1405                                                this.filter_extension_entries(cx);
1406                                                this.scroll_to_top(cx);
1407                                            }))
1408                                            .tooltip(move |_, cx| {
1409                                                Tooltip::simple("Show not installed extensions", cx)
1410                                            })
1411                                            .last(),
1412                                    ),
1413                            ),
1414                    ),
1415            )
1416            .child(
1417                h_flex()
1418                    .id("filter-row")
1419                    .gap_2()
1420                    .py_2p5()
1421                    .px_4()
1422                    .border_b_1()
1423                    .border_color(cx.theme().colors().border_variant)
1424                    .overflow_x_scroll()
1425                    .child(
1426                        Button::new("filter-all-categories", "All")
1427                            .when(self.provides_filter.is_none(), |button| {
1428                                button.style(ButtonStyle::Filled)
1429                            })
1430                            .when(self.provides_filter.is_some(), |button| {
1431                                button.style(ButtonStyle::Subtle)
1432                            })
1433                            .toggle_state(self.provides_filter.is_none())
1434                            .on_click(cx.listener(|this, _event, _, cx| {
1435                                this.change_provides_filter(None, cx);
1436                            })),
1437                    )
1438                    .children(ExtensionProvides::iter().map(|provides| {
1439                        let label = extension_provides_label(provides);
1440                        Button::new(
1441                            SharedString::from(format!("filter-category-{}", label)),
1442                            label,
1443                        )
1444                        .style(if self.provides_filter == Some(provides) {
1445                            ButtonStyle::Filled
1446                        } else {
1447                            ButtonStyle::Subtle
1448                        })
1449                        .toggle_state(self.provides_filter == Some(provides))
1450                        .on_click({
1451                            cx.listener(move |this, _event, _, cx| {
1452                                this.change_provides_filter(Some(provides), cx);
1453                            })
1454                        })
1455                    })),
1456            )
1457            .child(self.render_feature_upsells(cx))
1458            .child(
1459                v_flex()
1460                    .pl_4()
1461                    .pr_6()
1462                    .size_full()
1463                    .overflow_y_hidden()
1464                    .map(|this| {
1465                        let mut count = self.filtered_remote_extension_indices.len();
1466                        if self.filter.include_dev_extensions() {
1467                            count += self.dev_extension_entries.len();
1468                        }
1469
1470                        if count == 0 {
1471                            return this.py_4().child(self.render_empty_state(cx));
1472                        }
1473
1474                        let extensions_page = cx.entity().clone();
1475                        let scroll_handle = self.list.clone();
1476                        this.child(
1477                            uniform_list(
1478                                extensions_page,
1479                                "entries",
1480                                count,
1481                                Self::render_extensions,
1482                            )
1483                            .flex_grow()
1484                            .pb_4()
1485                            .track_scroll(scroll_handle),
1486                        )
1487                        .child(
1488                            div()
1489                                .absolute()
1490                                .right_1()
1491                                .top_0()
1492                                .bottom_0()
1493                                .w(px(12.))
1494                                .children(Scrollbar::vertical(self.scrollbar_state.clone())),
1495                        )
1496                    }),
1497            )
1498    }
1499}
1500
1501impl EventEmitter<ItemEvent> for ExtensionsPage {}
1502
1503impl Focusable for ExtensionsPage {
1504    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
1505        self.query_editor.read(cx).focus_handle(cx)
1506    }
1507}
1508
1509impl Item for ExtensionsPage {
1510    type Event = ItemEvent;
1511
1512    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1513        "Extensions".into()
1514    }
1515
1516    fn telemetry_event_text(&self) -> Option<&'static str> {
1517        Some("Extensions Page Opened")
1518    }
1519
1520    fn show_toolbar(&self) -> bool {
1521        false
1522    }
1523
1524    fn clone_on_split(
1525        &self,
1526        _workspace_id: Option<WorkspaceId>,
1527        _window: &mut Window,
1528        _: &mut Context<Self>,
1529    ) -> Option<Entity<Self>> {
1530        None
1531    }
1532
1533    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
1534        f(*event)
1535    }
1536}