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