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