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