extensions_ui.rs

   1mod components;
   2mod extension_suggest;
   3mod extension_version_selector;
   4
   5use crate::components::ExtensionCard;
   6use crate::extension_version_selector::{
   7    ExtensionVersionSelector, ExtensionVersionSelectorDelegate,
   8};
   9use client::telemetry::Telemetry;
  10use client::ExtensionMetadata;
  11use editor::{Editor, EditorElement, EditorStyle};
  12use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore};
  13use fuzzy::{match_strings, StringMatchCandidate};
  14use gpui::{
  15    actions, canvas, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
  16    InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
  17    UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
  18};
  19use release_channel::ReleaseChannel;
  20use settings::Settings;
  21use std::ops::DerefMut;
  22use std::time::Duration;
  23use std::{ops::Range, sync::Arc};
  24use theme::ThemeSettings;
  25use ui::{popover_menu, prelude::*, ContextMenu, ToggleButton, Tooltip};
  26use util::ResultExt as _;
  27use workspace::item::TabContentParams;
  28use workspace::{
  29    item::{Item, ItemEvent},
  30    Workspace, WorkspaceId,
  31};
  32
  33actions!(zed, [Extensions, InstallDevExtension]);
  34
  35pub fn init(cx: &mut AppContext) {
  36    cx.observe_new_views(move |workspace: &mut Workspace, cx| {
  37        workspace
  38            .register_action(move |workspace, _: &Extensions, cx| {
  39                let existing = workspace
  40                    .active_pane()
  41                    .read(cx)
  42                    .items()
  43                    .find_map(|item| item.downcast::<ExtensionsPage>());
  44
  45                if let Some(existing) = existing {
  46                    workspace.activate_item(&existing, cx);
  47                } else {
  48                    let extensions_page = ExtensionsPage::new(workspace, cx);
  49                    workspace.add_item_to_active_pane(Box::new(extensions_page), None, cx)
  50                }
  51            })
  52            .register_action(move |_, _: &InstallDevExtension, cx| {
  53                let store = ExtensionStore::global(cx);
  54                let prompt = cx.prompt_for_paths(gpui::PathPromptOptions {
  55                    files: false,
  56                    directories: true,
  57                    multiple: false,
  58                });
  59
  60                cx.deref_mut()
  61                    .spawn(|mut cx| async move {
  62                        let extension_path = prompt.await.log_err()??.pop()?;
  63                        store
  64                            .update(&mut cx, |store, cx| {
  65                                store
  66                                    .install_dev_extension(extension_path, cx)
  67                                    .detach_and_log_err(cx)
  68                            })
  69                            .ok()?;
  70                        Some(())
  71                    })
  72                    .detach();
  73            });
  74
  75        cx.subscribe(workspace.project(), |_, _, event, cx| match event {
  76            project::Event::LanguageNotFound(buffer) => {
  77                extension_suggest::suggest(buffer.clone(), cx);
  78            }
  79            _ => {}
  80        })
  81        .detach();
  82    })
  83    .detach();
  84}
  85
  86#[derive(Clone)]
  87pub enum ExtensionStatus {
  88    NotInstalled,
  89    Installing,
  90    Upgrading,
  91    Installed(Arc<str>),
  92    Removing,
  93}
  94
  95#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
  96enum ExtensionFilter {
  97    All,
  98    Installed,
  99    NotInstalled,
 100}
 101
 102impl ExtensionFilter {
 103    pub fn include_dev_extensions(&self) -> bool {
 104        match self {
 105            Self::All | Self::Installed => true,
 106            Self::NotInstalled => false,
 107        }
 108    }
 109}
 110
 111pub struct ExtensionsPage {
 112    workspace: WeakView<Workspace>,
 113    list: UniformListScrollHandle,
 114    telemetry: Arc<Telemetry>,
 115    is_fetching_extensions: bool,
 116    filter: ExtensionFilter,
 117    remote_extension_entries: Vec<ExtensionMetadata>,
 118    dev_extension_entries: Vec<Arc<ExtensionManifest>>,
 119    filtered_remote_extension_indices: Vec<usize>,
 120    query_editor: View<Editor>,
 121    query_contains_error: bool,
 122    _subscriptions: [gpui::Subscription; 2],
 123    extension_fetch_task: Option<Task<()>>,
 124}
 125
 126impl ExtensionsPage {
 127    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
 128        cx.new_view(|cx: &mut ViewContext<Self>| {
 129            let store = ExtensionStore::global(cx);
 130            let workspace_handle = workspace.weak_handle();
 131            let subscriptions = [
 132                cx.observe(&store, |_, _, cx| cx.notify()),
 133                cx.subscribe(&store, move |this, _, event, cx| match event {
 134                    extension::Event::ExtensionsUpdated => this.fetch_extensions_debounced(cx),
 135                    extension::Event::ExtensionInstalled(extension_id) => {
 136                        this.on_extension_installed(workspace_handle.clone(), extension_id, cx)
 137                    }
 138                    _ => {}
 139                }),
 140            ];
 141
 142            let query_editor = cx.new_view(|cx| {
 143                let mut input = Editor::single_line(cx);
 144                input.set_placeholder_text("Search extensions...", cx);
 145                input
 146            });
 147            cx.subscribe(&query_editor, Self::on_query_change).detach();
 148
 149            let mut this = Self {
 150                workspace: workspace.weak_handle(),
 151                list: UniformListScrollHandle::new(),
 152                telemetry: workspace.client().telemetry().clone(),
 153                is_fetching_extensions: false,
 154                filter: ExtensionFilter::All,
 155                dev_extension_entries: Vec::new(),
 156                filtered_remote_extension_indices: Vec::new(),
 157                remote_extension_entries: Vec::new(),
 158                query_contains_error: false,
 159                extension_fetch_task: None,
 160                _subscriptions: subscriptions,
 161                query_editor,
 162            };
 163            this.fetch_extensions(None, cx);
 164            this
 165        })
 166    }
 167
 168    fn on_extension_installed(
 169        &mut self,
 170        workspace: WeakView<Workspace>,
 171        extension_id: &str,
 172        cx: &mut ViewContext<Self>,
 173    ) {
 174        let extension_store = ExtensionStore::global(cx).read(cx);
 175        let themes = extension_store
 176            .extension_themes(extension_id)
 177            .map(|name| name.to_string())
 178            .collect::<Vec<_>>();
 179        if !themes.is_empty() {
 180            workspace
 181                .update(cx, |workspace, cx| {
 182                    theme_selector::toggle(
 183                        workspace,
 184                        &theme_selector::Toggle {
 185                            themes_filter: Some(themes),
 186                        },
 187                        cx,
 188                    )
 189                })
 190                .ok();
 191        }
 192    }
 193
 194    /// Returns whether a dev extension currently exists for the extension with the given ID.
 195    fn dev_extension_exists(extension_id: &str, cx: &mut ViewContext<Self>) -> bool {
 196        let extension_store = ExtensionStore::global(cx).read(cx);
 197
 198        extension_store
 199            .dev_extensions()
 200            .any(|dev_extension| dev_extension.id.as_ref() == extension_id)
 201    }
 202
 203    fn extension_status(extension_id: &str, cx: &mut ViewContext<Self>) -> ExtensionStatus {
 204        let extension_store = ExtensionStore::global(cx).read(cx);
 205
 206        match extension_store.outstanding_operations().get(extension_id) {
 207            Some(ExtensionOperation::Install) => ExtensionStatus::Installing,
 208            Some(ExtensionOperation::Remove) => ExtensionStatus::Removing,
 209            Some(ExtensionOperation::Upgrade) => ExtensionStatus::Upgrading,
 210            None => match extension_store.installed_extensions().get(extension_id) {
 211                Some(extension) => ExtensionStatus::Installed(extension.manifest.version.clone()),
 212                None => ExtensionStatus::NotInstalled,
 213            },
 214        }
 215    }
 216
 217    fn filter_extension_entries(&mut self, cx: &mut ViewContext<Self>) {
 218        self.filtered_remote_extension_indices.clear();
 219        self.filtered_remote_extension_indices.extend(
 220            self.remote_extension_entries
 221                .iter()
 222                .enumerate()
 223                .filter(|(_, extension)| match self.filter {
 224                    ExtensionFilter::All => true,
 225                    ExtensionFilter::Installed => {
 226                        let status = Self::extension_status(&extension.id, cx);
 227                        matches!(status, ExtensionStatus::Installed(_))
 228                    }
 229                    ExtensionFilter::NotInstalled => {
 230                        let status = Self::extension_status(&extension.id, cx);
 231
 232                        matches!(status, ExtensionStatus::NotInstalled)
 233                    }
 234                })
 235                .map(|(ix, _)| ix),
 236        );
 237        cx.notify();
 238    }
 239
 240    fn fetch_extensions(&mut self, search: Option<String>, cx: &mut ViewContext<Self>) {
 241        self.is_fetching_extensions = true;
 242        cx.notify();
 243
 244        let extension_store = ExtensionStore::global(cx);
 245
 246        let dev_extensions = extension_store.update(cx, |store, _| {
 247            store.dev_extensions().cloned().collect::<Vec<_>>()
 248        });
 249
 250        let remote_extensions = extension_store.update(cx, |store, cx| {
 251            store.fetch_extensions(search.as_deref(), cx)
 252        });
 253
 254        cx.spawn(move |this, mut cx| async move {
 255            let dev_extensions = if let Some(search) = search {
 256                let match_candidates = dev_extensions
 257                    .iter()
 258                    .enumerate()
 259                    .map(|(ix, manifest)| StringMatchCandidate {
 260                        id: ix,
 261                        string: manifest.name.clone(),
 262                        char_bag: manifest.name.as_str().into(),
 263                    })
 264                    .collect::<Vec<_>>();
 265
 266                let matches = match_strings(
 267                    &match_candidates,
 268                    &search,
 269                    false,
 270                    match_candidates.len(),
 271                    &Default::default(),
 272                    cx.background_executor().clone(),
 273                )
 274                .await;
 275                matches
 276                    .into_iter()
 277                    .map(|mat| dev_extensions[mat.candidate_id].clone())
 278                    .collect()
 279            } else {
 280                dev_extensions
 281            };
 282
 283            let fetch_result = remote_extensions.await;
 284            this.update(&mut cx, |this, cx| {
 285                cx.notify();
 286                this.dev_extension_entries = dev_extensions;
 287                this.is_fetching_extensions = false;
 288                this.remote_extension_entries = fetch_result?;
 289                this.filter_extension_entries(cx);
 290                anyhow::Ok(())
 291            })?
 292        })
 293        .detach_and_log_err(cx);
 294    }
 295
 296    fn render_extensions(
 297        &mut self,
 298        range: Range<usize>,
 299        cx: &mut ViewContext<Self>,
 300    ) -> Vec<ExtensionCard> {
 301        let dev_extension_entries_len = if self.filter.include_dev_extensions() {
 302            self.dev_extension_entries.len()
 303        } else {
 304            0
 305        };
 306        range
 307            .map(|ix| {
 308                if ix < dev_extension_entries_len {
 309                    let extension = &self.dev_extension_entries[ix];
 310                    self.render_dev_extension(extension, cx)
 311                } else {
 312                    let extension_ix =
 313                        self.filtered_remote_extension_indices[ix - dev_extension_entries_len];
 314                    let extension = &self.remote_extension_entries[extension_ix];
 315                    self.render_remote_extension(extension, cx)
 316                }
 317            })
 318            .collect()
 319    }
 320
 321    fn render_dev_extension(
 322        &self,
 323        extension: &ExtensionManifest,
 324        cx: &mut ViewContext<Self>,
 325    ) -> ExtensionCard {
 326        let status = Self::extension_status(&extension.id, cx);
 327
 328        let repository_url = extension.repository.clone();
 329
 330        ExtensionCard::new()
 331            .child(
 332                h_flex()
 333                    .justify_between()
 334                    .child(
 335                        h_flex()
 336                            .gap_2()
 337                            .items_end()
 338                            .child(Headline::new(extension.name.clone()).size(HeadlineSize::Medium))
 339                            .child(
 340                                Headline::new(format!("v{}", extension.version))
 341                                    .size(HeadlineSize::XSmall),
 342                            ),
 343                    )
 344                    .child(
 345                        h_flex()
 346                            .gap_2()
 347                            .justify_between()
 348                            .child(
 349                                Button::new(
 350                                    SharedString::from(format!("rebuild-{}", extension.id)),
 351                                    "Rebuild",
 352                                )
 353                                .on_click({
 354                                    let extension_id = extension.id.clone();
 355                                    move |_, cx| {
 356                                        ExtensionStore::global(cx).update(cx, |store, cx| {
 357                                            store.rebuild_dev_extension(extension_id.clone(), cx)
 358                                        });
 359                                    }
 360                                })
 361                                .color(Color::Accent)
 362                                .disabled(matches!(status, ExtensionStatus::Upgrading)),
 363                            )
 364                            .child(
 365                                Button::new(SharedString::from(extension.id.clone()), "Uninstall")
 366                                    .on_click({
 367                                        let extension_id = extension.id.clone();
 368                                        move |_, cx| {
 369                                            ExtensionStore::global(cx).update(cx, |store, cx| {
 370                                                store.uninstall_extension(extension_id.clone(), cx)
 371                                            });
 372                                        }
 373                                    })
 374                                    .color(Color::Accent)
 375                                    .disabled(matches!(status, ExtensionStatus::Removing)),
 376                            ),
 377                    ),
 378            )
 379            .child(
 380                h_flex()
 381                    .justify_between()
 382                    .child(
 383                        Label::new(format!(
 384                            "{}: {}",
 385                            if extension.authors.len() > 1 {
 386                                "Authors"
 387                            } else {
 388                                "Author"
 389                            },
 390                            extension.authors.join(", ")
 391                        ))
 392                        .size(LabelSize::Small),
 393                    )
 394                    .child(Label::new("<>").size(LabelSize::Small)),
 395            )
 396            .child(
 397                h_flex()
 398                    .justify_between()
 399                    .children(extension.description.as_ref().map(|description| {
 400                        Label::new(description.clone())
 401                            .size(LabelSize::Small)
 402                            .color(Color::Default)
 403                    }))
 404                    .children(repository_url.map(|repository_url| {
 405                        IconButton::new(
 406                            SharedString::from(format!("repository-{}", extension.id)),
 407                            IconName::Github,
 408                        )
 409                        .icon_color(Color::Accent)
 410                        .icon_size(IconSize::Small)
 411                        .style(ButtonStyle::Filled)
 412                        .on_click(cx.listener({
 413                            let repository_url = repository_url.clone();
 414                            move |_, _, cx| {
 415                                cx.open_url(&repository_url);
 416                            }
 417                        }))
 418                        .tooltip(move |cx| Tooltip::text(repository_url.clone(), cx))
 419                    })),
 420            )
 421    }
 422
 423    fn render_remote_extension(
 424        &self,
 425        extension: &ExtensionMetadata,
 426        cx: &mut ViewContext<Self>,
 427    ) -> ExtensionCard {
 428        let this = cx.view().clone();
 429        let status = Self::extension_status(&extension.id, cx);
 430        let has_dev_extension = Self::dev_extension_exists(&extension.id, cx);
 431
 432        let extension_id = extension.id.clone();
 433        let (install_or_uninstall_button, upgrade_button) =
 434            self.buttons_for_entry(extension, &status, has_dev_extension, cx);
 435        let version = extension.manifest.version.clone();
 436        let repository_url = extension.manifest.repository.clone();
 437
 438        let installed_version = match status {
 439            ExtensionStatus::Installed(installed_version) => Some(installed_version),
 440            _ => None,
 441        };
 442
 443        ExtensionCard::new()
 444            .overridden_by_dev_extension(has_dev_extension)
 445            .child(
 446                h_flex()
 447                    .justify_between()
 448                    .child(
 449                        h_flex()
 450                            .gap_2()
 451                            .items_end()
 452                            .child(
 453                                Headline::new(extension.manifest.name.clone())
 454                                    .size(HeadlineSize::Medium),
 455                            )
 456                            .child(Headline::new(format!("v{version}")).size(HeadlineSize::XSmall))
 457                            .children(
 458                                installed_version
 459                                    .filter(|installed_version| *installed_version != version)
 460                                    .map(|installed_version| {
 461                                        Headline::new(format!("(v{installed_version} installed)",))
 462                                            .size(HeadlineSize::XSmall)
 463                                    }),
 464                            ),
 465                    )
 466                    .child(
 467                        h_flex()
 468                            .gap_2()
 469                            .justify_between()
 470                            .children(upgrade_button)
 471                            .child(install_or_uninstall_button),
 472                    ),
 473            )
 474            .child(
 475                h_flex()
 476                    .justify_between()
 477                    .child(
 478                        Label::new(format!(
 479                            "{}: {}",
 480                            if extension.manifest.authors.len() > 1 {
 481                                "Authors"
 482                            } else {
 483                                "Author"
 484                            },
 485                            extension.manifest.authors.join(", ")
 486                        ))
 487                        .size(LabelSize::Small),
 488                    )
 489                    .child(
 490                        Label::new(format!("Downloads: {}", extension.download_count))
 491                            .size(LabelSize::Small),
 492                    ),
 493            )
 494            .child(
 495                h_flex()
 496                    .gap_2()
 497                    .justify_between()
 498                    .children(extension.manifest.description.as_ref().map(|description| {
 499                        h_flex().overflow_x_hidden().child(
 500                            Label::new(description.clone())
 501                                .size(LabelSize::Small)
 502                                .color(Color::Default),
 503                        )
 504                    }))
 505                    .child(
 506                        h_flex()
 507                            .gap_2()
 508                            .child(
 509                                IconButton::new(
 510                                    SharedString::from(format!("repository-{}", extension.id)),
 511                                    IconName::Github,
 512                                )
 513                                .icon_color(Color::Accent)
 514                                .icon_size(IconSize::Small)
 515                                .style(ButtonStyle::Filled)
 516                                .on_click(cx.listener({
 517                                    let repository_url = repository_url.clone();
 518                                    move |_, _, cx| {
 519                                        cx.open_url(&repository_url);
 520                                    }
 521                                }))
 522                                .tooltip(move |cx| Tooltip::text(repository_url.clone(), cx)),
 523                            )
 524                            .child(
 525                                popover_menu(SharedString::from(format!("more-{}", extension.id)))
 526                                    .trigger(
 527                                        IconButton::new(
 528                                            SharedString::from(format!("more-{}", extension.id)),
 529                                            IconName::Ellipsis,
 530                                        )
 531                                        .icon_color(Color::Accent)
 532                                        .icon_size(IconSize::Small)
 533                                        .style(ButtonStyle::Filled),
 534                                    )
 535                                    .menu(move |cx| {
 536                                        Some(Self::render_remote_extension_context_menu(
 537                                            &this,
 538                                            extension_id.clone(),
 539                                            cx,
 540                                        ))
 541                                    }),
 542                            ),
 543                    ),
 544            )
 545    }
 546
 547    fn render_remote_extension_context_menu(
 548        this: &View<Self>,
 549        extension_id: Arc<str>,
 550        cx: &mut WindowContext,
 551    ) -> View<ContextMenu> {
 552        let context_menu = ContextMenu::build(cx, |context_menu, cx| {
 553            context_menu.entry(
 554                "Install Another Version...",
 555                None,
 556                cx.handler_for(&this, move |this, cx| {
 557                    this.show_extension_version_list(extension_id.clone(), cx)
 558                }),
 559            )
 560        });
 561
 562        context_menu
 563    }
 564
 565    fn show_extension_version_list(&mut self, extension_id: Arc<str>, cx: &mut ViewContext<Self>) {
 566        let Some(workspace) = self.workspace.upgrade() else {
 567            return;
 568        };
 569
 570        cx.spawn(move |this, mut cx| async move {
 571            let extension_versions_task = this.update(&mut cx, |_, cx| {
 572                let extension_store = ExtensionStore::global(cx);
 573
 574                extension_store.update(cx, |store, cx| {
 575                    store.fetch_extension_versions(&extension_id, cx)
 576                })
 577            })?;
 578
 579            let extension_versions = extension_versions_task.await?;
 580
 581            workspace.update(&mut cx, |workspace, cx| {
 582                let fs = workspace.project().read(cx).fs().clone();
 583                workspace.toggle_modal(cx, |cx| {
 584                    let delegate = ExtensionVersionSelectorDelegate::new(
 585                        fs,
 586                        cx.view().downgrade(),
 587                        extension_versions,
 588                    );
 589
 590                    ExtensionVersionSelector::new(delegate, cx)
 591                });
 592            })?;
 593
 594            anyhow::Ok(())
 595        })
 596        .detach_and_log_err(cx);
 597    }
 598
 599    fn buttons_for_entry(
 600        &self,
 601        extension: &ExtensionMetadata,
 602        status: &ExtensionStatus,
 603        has_dev_extension: bool,
 604        cx: &mut ViewContext<Self>,
 605    ) -> (Button, Option<Button>) {
 606        let is_compatible =
 607            extension::is_version_compatible(ReleaseChannel::global(cx), &extension);
 608
 609        if has_dev_extension {
 610            // If we have a dev extension for the given extension, just treat it as uninstalled.
 611            // The button here is a placeholder, as it won't be interactable anyways.
 612            return (
 613                Button::new(SharedString::from(extension.id.clone()), "Install"),
 614                None,
 615            );
 616        }
 617
 618        match status.clone() {
 619            ExtensionStatus::NotInstalled => (
 620                Button::new(SharedString::from(extension.id.clone()), "Install").on_click(
 621                    cx.listener({
 622                        let extension_id = extension.id.clone();
 623                        move |this, _, cx| {
 624                            this.telemetry
 625                                .report_app_event("extensions: install extension".to_string());
 626                            ExtensionStore::global(cx).update(cx, |store, cx| {
 627                                store.install_latest_extension(extension_id.clone(), cx)
 628                            });
 629                        }
 630                    }),
 631                ),
 632                None,
 633            ),
 634            ExtensionStatus::Installing => (
 635                Button::new(SharedString::from(extension.id.clone()), "Install").disabled(true),
 636                None,
 637            ),
 638            ExtensionStatus::Upgrading => (
 639                Button::new(SharedString::from(extension.id.clone()), "Uninstall").disabled(true),
 640                Some(
 641                    Button::new(SharedString::from(extension.id.clone()), "Upgrade").disabled(true),
 642                ),
 643            ),
 644            ExtensionStatus::Installed(installed_version) => (
 645                Button::new(SharedString::from(extension.id.clone()), "Uninstall").on_click(
 646                    cx.listener({
 647                        let extension_id = extension.id.clone();
 648                        move |this, _, cx| {
 649                            this.telemetry
 650                                .report_app_event("extensions: uninstall extension".to_string());
 651                            ExtensionStore::global(cx).update(cx, |store, cx| {
 652                                store.uninstall_extension(extension_id.clone(), cx)
 653                            });
 654                        }
 655                    }),
 656                ),
 657                if installed_version == extension.manifest.version {
 658                    None
 659                } else {
 660                    Some(
 661                        Button::new(SharedString::from(extension.id.clone()), "Upgrade")
 662                            .when(!is_compatible, |upgrade_button| {
 663                                upgrade_button.disabled(true).tooltip({
 664                                    let version = extension.manifest.version.clone();
 665                                    move |cx| {
 666                                        Tooltip::text(
 667                                            format!(
 668                                                "v{version} is not compatible with this version of Zed.",
 669                                            ),
 670                                            cx,
 671                                        )
 672                                    }
 673                                })
 674                            })
 675                            .disabled(!is_compatible)
 676                            .on_click(cx.listener({
 677                                let extension_id = extension.id.clone();
 678                                let version = extension.manifest.version.clone();
 679                                move |this, _, cx| {
 680                                    this.telemetry.report_app_event(
 681                                        "extensions: install extension".to_string(),
 682                                    );
 683                                    ExtensionStore::global(cx).update(cx, |store, cx| {
 684                                        store
 685                                            .upgrade_extension(
 686                                                extension_id.clone(),
 687                                                version.clone(),
 688                                                cx,
 689                                            )
 690                                            .detach_and_log_err(cx)
 691                                    });
 692                                }
 693                            })),
 694                    )
 695                },
 696            ),
 697            ExtensionStatus::Removing => (
 698                Button::new(SharedString::from(extension.id.clone()), "Uninstall").disabled(true),
 699                None,
 700            ),
 701        }
 702    }
 703
 704    fn render_search(&self, cx: &mut ViewContext<Self>) -> Div {
 705        let mut key_context = KeyContext::new_with_defaults();
 706        key_context.add("BufferSearchBar");
 707
 708        let editor_border = if self.query_contains_error {
 709            Color::Error.color(cx)
 710        } else {
 711            cx.theme().colors().border
 712        };
 713
 714        h_flex()
 715            .w_full()
 716            .gap_2()
 717            .key_context(key_context)
 718            // .capture_action(cx.listener(Self::tab))
 719            // .on_action(cx.listener(Self::dismiss))
 720            .child(
 721                h_flex()
 722                    .flex_1()
 723                    .px_2()
 724                    .py_1()
 725                    .gap_2()
 726                    .border_1()
 727                    .border_color(editor_border)
 728                    .min_w(rems_from_px(384.))
 729                    .rounded_lg()
 730                    .child(Icon::new(IconName::MagnifyingGlass))
 731                    .child(self.render_text_input(&self.query_editor, cx)),
 732            )
 733    }
 734
 735    fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
 736        let settings = ThemeSettings::get_global(cx);
 737        let text_style = TextStyle {
 738            color: if editor.read(cx).read_only(cx) {
 739                cx.theme().colors().text_disabled
 740            } else {
 741                cx.theme().colors().text
 742            },
 743            font_family: settings.ui_font.family.clone(),
 744            font_features: settings.ui_font.features.clone(),
 745            font_size: rems(0.875).into(),
 746            font_weight: settings.ui_font.weight,
 747            font_style: FontStyle::Normal,
 748            line_height: relative(1.3),
 749            background_color: None,
 750            underline: None,
 751            strikethrough: None,
 752            white_space: WhiteSpace::Normal,
 753        };
 754
 755        EditorElement::new(
 756            &editor,
 757            EditorStyle {
 758                background: cx.theme().colors().editor_background,
 759                local_player: cx.theme().players().local(),
 760                text: text_style,
 761                ..Default::default()
 762            },
 763        )
 764    }
 765
 766    fn on_query_change(
 767        &mut self,
 768        _: View<Editor>,
 769        event: &editor::EditorEvent,
 770        cx: &mut ViewContext<Self>,
 771    ) {
 772        if let editor::EditorEvent::Edited = event {
 773            self.query_contains_error = false;
 774            self.fetch_extensions_debounced(cx);
 775        }
 776    }
 777
 778    fn fetch_extensions_debounced(&mut self, cx: &mut ViewContext<'_, ExtensionsPage>) {
 779        self.extension_fetch_task = Some(cx.spawn(|this, mut cx| async move {
 780            let search = this
 781                .update(&mut cx, |this, cx| this.search_query(cx))
 782                .ok()
 783                .flatten();
 784
 785            // Only debounce the fetching of extensions if we have a search
 786            // query.
 787            //
 788            // If the search was just cleared then we can just reload the list
 789            // of extensions without a debounce, which allows us to avoid seeing
 790            // an intermittent flash of a "no extensions" state.
 791            if let Some(_) = search {
 792                cx.background_executor()
 793                    .timer(Duration::from_millis(250))
 794                    .await;
 795            };
 796
 797            this.update(&mut cx, |this, cx| {
 798                this.fetch_extensions(search, cx);
 799            })
 800            .ok();
 801        }));
 802    }
 803
 804    pub fn search_query(&self, cx: &WindowContext) -> Option<String> {
 805        let search = self.query_editor.read(cx).text(cx);
 806        if search.trim().is_empty() {
 807            None
 808        } else {
 809            Some(search)
 810        }
 811    }
 812
 813    fn render_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 814        let has_search = self.search_query(cx).is_some();
 815
 816        let message = if self.is_fetching_extensions {
 817            "Loading extensions..."
 818        } else {
 819            match self.filter {
 820                ExtensionFilter::All => {
 821                    if has_search {
 822                        "No extensions that match your search."
 823                    } else {
 824                        "No extensions."
 825                    }
 826                }
 827                ExtensionFilter::Installed => {
 828                    if has_search {
 829                        "No installed extensions that match your search."
 830                    } else {
 831                        "No installed extensions."
 832                    }
 833                }
 834                ExtensionFilter::NotInstalled => {
 835                    if has_search {
 836                        "No not installed extensions that match your search."
 837                    } else {
 838                        "No not installed extensions."
 839                    }
 840                }
 841            }
 842        };
 843
 844        Label::new(message)
 845    }
 846}
 847
 848impl Render for ExtensionsPage {
 849    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 850        v_flex()
 851            .size_full()
 852            .bg(cx.theme().colors().editor_background)
 853            .child(
 854                v_flex()
 855                    .gap_4()
 856                    .p_4()
 857                    .border_b_1()
 858                    .border_color(cx.theme().colors().border)
 859                    .bg(cx.theme().colors().editor_background)
 860                    .child(
 861                        h_flex()
 862                            .w_full()
 863                            .gap_2()
 864                            .justify_between()
 865                            .child(Headline::new("Extensions").size(HeadlineSize::XLarge))
 866                            .child(
 867                                Button::new("install-dev-extension", "Install Dev Extension")
 868                                    .style(ButtonStyle::Filled)
 869                                    .size(ButtonSize::Large)
 870                                    .on_click(|_event, cx| {
 871                                        cx.dispatch_action(Box::new(InstallDevExtension))
 872                                    }),
 873                            ),
 874                    )
 875                    .child(
 876                        h_flex()
 877                            .w_full()
 878                            .gap_2()
 879                            .justify_between()
 880                            .child(h_flex().child(self.render_search(cx)))
 881                            .child(
 882                                h_flex()
 883                                    .child(
 884                                        ToggleButton::new("filter-all", "All")
 885                                            .style(ButtonStyle::Filled)
 886                                            .size(ButtonSize::Large)
 887                                            .selected(self.filter == ExtensionFilter::All)
 888                                            .on_click(cx.listener(|this, _event, cx| {
 889                                                this.filter = ExtensionFilter::All;
 890                                                this.filter_extension_entries(cx);
 891                                            }))
 892                                            .tooltip(move |cx| {
 893                                                Tooltip::text("Show all extensions", cx)
 894                                            })
 895                                            .first(),
 896                                    )
 897                                    .child(
 898                                        ToggleButton::new("filter-installed", "Installed")
 899                                            .style(ButtonStyle::Filled)
 900                                            .size(ButtonSize::Large)
 901                                            .selected(self.filter == ExtensionFilter::Installed)
 902                                            .on_click(cx.listener(|this, _event, cx| {
 903                                                this.filter = ExtensionFilter::Installed;
 904                                                this.filter_extension_entries(cx);
 905                                            }))
 906                                            .tooltip(move |cx| {
 907                                                Tooltip::text("Show installed extensions", cx)
 908                                            })
 909                                            .middle(),
 910                                    )
 911                                    .child(
 912                                        ToggleButton::new("filter-not-installed", "Not Installed")
 913                                            .style(ButtonStyle::Filled)
 914                                            .size(ButtonSize::Large)
 915                                            .selected(self.filter == ExtensionFilter::NotInstalled)
 916                                            .on_click(cx.listener(|this, _event, cx| {
 917                                                this.filter = ExtensionFilter::NotInstalled;
 918                                                this.filter_extension_entries(cx);
 919                                            }))
 920                                            .tooltip(move |cx| {
 921                                                Tooltip::text("Show not installed extensions", cx)
 922                                            })
 923                                            .last(),
 924                                    ),
 925                            ),
 926                    ),
 927            )
 928            .child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| {
 929                let mut count = self.filtered_remote_extension_indices.len();
 930                if self.filter.include_dev_extensions() {
 931                    count += self.dev_extension_entries.len();
 932                }
 933
 934                if count == 0 {
 935                    return this.py_4().child(self.render_empty_state(cx));
 936                }
 937
 938                let view = cx.view().clone();
 939                let scroll_handle = self.list.clone();
 940                this.child(
 941                    canvas(
 942                        move |bounds, cx| {
 943                            let mut list = uniform_list::<_, ExtensionCard, _>(
 944                                view,
 945                                "entries",
 946                                count,
 947                                Self::render_extensions,
 948                            )
 949                            .size_full()
 950                            .pb_4()
 951                            .track_scroll(scroll_handle)
 952                            .into_any_element();
 953                            list.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
 954                            list
 955                        },
 956                        |_bounds, mut list, cx| list.paint(cx),
 957                    )
 958                    .size_full(),
 959                )
 960            }))
 961    }
 962}
 963
 964impl EventEmitter<ItemEvent> for ExtensionsPage {}
 965
 966impl FocusableView for ExtensionsPage {
 967    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
 968        self.query_editor.read(cx).focus_handle(cx)
 969    }
 970}
 971
 972impl Item for ExtensionsPage {
 973    type Event = ItemEvent;
 974
 975    fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement {
 976        Label::new("Extensions")
 977            .color(if params.selected {
 978                Color::Default
 979            } else {
 980                Color::Muted
 981            })
 982            .into_any_element()
 983    }
 984
 985    fn telemetry_event_text(&self) -> Option<&'static str> {
 986        Some("extensions page")
 987    }
 988
 989    fn show_toolbar(&self) -> bool {
 990        false
 991    }
 992
 993    fn clone_on_split(
 994        &self,
 995        _workspace_id: WorkspaceId,
 996        _: &mut ViewContext<Self>,
 997    ) -> Option<View<Self>> {
 998        None
 999    }
1000
1001    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
1002        f(*event)
1003    }
1004}