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