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