extensions_ui.rs

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