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