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}