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