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