component_preview.rs

   1//! # Component Preview
   2//!
   3//! A view for exploring Zed components.
   4
   5mod persistence;
   6mod preview_support;
   7
   8use std::iter::Iterator;
   9use std::sync::Arc;
  10
  11use agent::{ActiveThread, ThreadStore};
  12use client::UserStore;
  13use component::{ComponentId, ComponentMetadata, components};
  14use gpui::{
  15    App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, list, prelude::*,
  16};
  17
  18use collections::HashMap;
  19
  20use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle};
  21use languages::LanguageRegistry;
  22use notifications::status_toast::{StatusToast, ToastIcon};
  23use persistence::COMPONENT_PREVIEW_DB;
  24use preview_support::active_thread::{load_preview_thread_store, static_active_thread};
  25use project::Project;
  26use ui::{Divider, HighlightedLabel, ListItem, ListSubHeader, prelude::*};
  27
  28use ui_input::SingleLineInput;
  29use workspace::{AppState, ItemId, SerializableItem, delete_unloaded_items};
  30use workspace::{Item, Workspace, WorkspaceId, item::ItemEvent};
  31
  32pub fn init(app_state: Arc<AppState>, cx: &mut App) {
  33    workspace::register_serializable_item::<ComponentPreview>(cx);
  34
  35    let app_state = app_state.clone();
  36
  37    cx.observe_new(move |workspace: &mut Workspace, _window, cx| {
  38        let app_state = app_state.clone();
  39        let project = workspace.project().clone();
  40        let weak_workspace = cx.entity().downgrade();
  41
  42        workspace.register_action(
  43            move |workspace, _: &workspace::OpenComponentPreview, window, cx| {
  44                let app_state = app_state.clone();
  45
  46                let language_registry = app_state.languages.clone();
  47                let user_store = app_state.user_store.clone();
  48
  49                let component_preview = cx.new(|cx| {
  50                    ComponentPreview::new(
  51                        weak_workspace.clone(),
  52                        project.clone(),
  53                        language_registry,
  54                        user_store,
  55                        None,
  56                        None,
  57                        window,
  58                        cx,
  59                    )
  60                    .expect("Failed to create component preview")
  61                });
  62
  63                workspace.add_item_to_active_pane(
  64                    Box::new(component_preview),
  65                    None,
  66                    true,
  67                    window,
  68                    cx,
  69                )
  70            },
  71        );
  72    })
  73    .detach();
  74}
  75
  76enum PreviewEntry {
  77    AllComponents,
  78    ActiveThread,
  79    Separator,
  80    Component(ComponentMetadata, Option<Vec<usize>>),
  81    SectionHeader(SharedString),
  82}
  83
  84impl From<ComponentMetadata> for PreviewEntry {
  85    fn from(component: ComponentMetadata) -> Self {
  86        PreviewEntry::Component(component, None)
  87    }
  88}
  89
  90impl From<SharedString> for PreviewEntry {
  91    fn from(section_header: SharedString) -> Self {
  92        PreviewEntry::SectionHeader(section_header)
  93    }
  94}
  95
  96#[derive(Default, Debug, Clone, PartialEq, Eq)]
  97enum PreviewPage {
  98    #[default]
  99    AllComponents,
 100    Component(ComponentId),
 101    ActiveThread,
 102}
 103
 104struct ComponentPreview {
 105    workspace_id: Option<WorkspaceId>,
 106    focus_handle: FocusHandle,
 107    _view_scroll_handle: ScrollHandle,
 108    nav_scroll_handle: UniformListScrollHandle,
 109    component_map: HashMap<ComponentId, ComponentMetadata>,
 110    active_page: PreviewPage,
 111    components: Vec<ComponentMetadata>,
 112    component_list: ListState,
 113    agent_previews: Vec<
 114        Box<
 115            dyn Fn(
 116                &Self,
 117                WeakEntity<Workspace>,
 118                Entity<ActiveThread>,
 119                WeakEntity<ThreadStore>,
 120                &mut Window,
 121                &mut App,
 122            ) -> Option<AnyElement>,
 123        >,
 124    >,
 125    cursor_index: usize,
 126    language_registry: Arc<LanguageRegistry>,
 127    workspace: WeakEntity<Workspace>,
 128    project: Entity<Project>,
 129    user_store: Entity<UserStore>,
 130    filter_editor: Entity<SingleLineInput>,
 131    filter_text: String,
 132
 133    // preview support
 134    thread_store: Option<Entity<ThreadStore>>,
 135    active_thread: Option<Entity<ActiveThread>>,
 136}
 137
 138impl ComponentPreview {
 139    pub fn new(
 140        workspace: WeakEntity<Workspace>,
 141        project: Entity<Project>,
 142        language_registry: Arc<LanguageRegistry>,
 143        user_store: Entity<UserStore>,
 144        selected_index: impl Into<Option<usize>>,
 145        active_page: Option<PreviewPage>,
 146        window: &mut Window,
 147        cx: &mut Context<Self>,
 148    ) -> anyhow::Result<Self> {
 149        let workspace_clone = workspace.clone();
 150        let project_clone = project.clone();
 151
 152        let entity = cx.weak_entity();
 153        window
 154            .spawn(cx, async move |cx| {
 155                let thread_store_task =
 156                    load_preview_thread_store(workspace_clone.clone(), project_clone.clone(), cx)
 157                        .await;
 158
 159                if let Ok(thread_store) = thread_store_task.await {
 160                    entity
 161                        .update_in(cx, |this, window, cx| {
 162                            this.thread_store = Some(thread_store.clone());
 163                            this.create_active_thread(window, cx);
 164                        })
 165                        .ok();
 166                }
 167            })
 168            .detach();
 169
 170        let sorted_components = components().all_sorted();
 171        let selected_index = selected_index.into().unwrap_or(0);
 172        let active_page = active_page.unwrap_or(PreviewPage::AllComponents);
 173        let filter_editor =
 174            cx.new(|cx| SingleLineInput::new(window, cx, "Find components or usages…"));
 175
 176        let component_list = ListState::new(
 177            sorted_components.len(),
 178            gpui::ListAlignment::Top,
 179            px(1500.0),
 180            {
 181                let this = cx.entity().downgrade();
 182                move |ix, window: &mut Window, cx: &mut App| {
 183                    this.update(cx, |this, cx| {
 184                        let component = this.get_component(ix);
 185                        this.render_preview(&component, window, cx)
 186                            .into_any_element()
 187                    })
 188                    .unwrap()
 189                }
 190            },
 191        );
 192
 193        // Initialize agent previews
 194        let agent_previews = agent::all_agent_previews()
 195            .into_iter()
 196            .map(|id| {
 197                Box::new(
 198                    move |_self: &ComponentPreview,
 199                          workspace: WeakEntity<Workspace>,
 200                          active_thread: Entity<ActiveThread>,
 201                          thread_store: WeakEntity<ThreadStore>,
 202                          window: &mut Window,
 203                          cx: &mut App| {
 204                        agent::get_agent_preview(
 205                            &id,
 206                            workspace,
 207                            active_thread,
 208                            thread_store,
 209                            window,
 210                            cx,
 211                        )
 212                    },
 213                )
 214                    as Box<
 215                        dyn Fn(
 216                            &ComponentPreview,
 217                            WeakEntity<Workspace>,
 218                            Entity<ActiveThread>,
 219                            WeakEntity<ThreadStore>,
 220                            &mut Window,
 221                            &mut App,
 222                        ) -> Option<AnyElement>,
 223                    >
 224            })
 225            .collect::<Vec<_>>();
 226
 227        let mut component_preview = Self {
 228            workspace_id: None,
 229            focus_handle: cx.focus_handle(),
 230            _view_scroll_handle: ScrollHandle::new(),
 231            nav_scroll_handle: UniformListScrollHandle::new(),
 232            language_registry,
 233            user_store,
 234            workspace,
 235            project,
 236            active_page,
 237            component_map: components().0,
 238            components: sorted_components,
 239            component_list,
 240            agent_previews,
 241            cursor_index: selected_index,
 242            filter_editor,
 243            filter_text: String::new(),
 244            thread_store: None,
 245            active_thread: None,
 246        };
 247
 248        if component_preview.cursor_index > 0 {
 249            component_preview.scroll_to_preview(component_preview.cursor_index, cx);
 250        }
 251
 252        component_preview.update_component_list(cx);
 253
 254        let focus_handle = component_preview.filter_editor.read(cx).focus_handle(cx);
 255        window.focus(&focus_handle);
 256
 257        Ok(component_preview)
 258    }
 259
 260    pub fn create_active_thread(
 261        &mut self,
 262        window: &mut Window,
 263        cx: &mut Context<Self>,
 264    ) -> &mut Self {
 265        let workspace = self.workspace.clone();
 266        let language_registry = self.language_registry.clone();
 267        let weak_handle = self.workspace.clone();
 268        if let Some(workspace) = workspace.upgrade() {
 269            let project = workspace.read(cx).project().clone();
 270            if let Some(thread_store) = self.thread_store.clone() {
 271                let active_thread = static_active_thread(
 272                    weak_handle,
 273                    project,
 274                    language_registry,
 275                    thread_store,
 276                    window,
 277                    cx,
 278                );
 279                self.active_thread = Some(active_thread);
 280                cx.notify();
 281            }
 282        }
 283
 284        self
 285    }
 286
 287    pub fn active_page_id(&self, _cx: &App) -> ActivePageId {
 288        match &self.active_page {
 289            PreviewPage::AllComponents => ActivePageId::default(),
 290            PreviewPage::Component(component_id) => ActivePageId(component_id.0.to_string()),
 291            PreviewPage::ActiveThread => ActivePageId("active_thread".to_string()),
 292        }
 293    }
 294
 295    fn scroll_to_preview(&mut self, ix: usize, cx: &mut Context<Self>) {
 296        self.component_list.scroll_to_reveal_item(ix);
 297        self.cursor_index = ix;
 298        cx.notify();
 299    }
 300
 301    fn set_active_page(&mut self, page: PreviewPage, cx: &mut Context<Self>) {
 302        self.active_page = page;
 303        cx.emit(ItemEvent::UpdateTab);
 304        cx.notify();
 305    }
 306
 307    fn get_component(&self, ix: usize) -> ComponentMetadata {
 308        self.components[ix].clone()
 309    }
 310
 311    fn filtered_components(&self) -> Vec<ComponentMetadata> {
 312        if self.filter_text.is_empty() {
 313            return self.components.clone();
 314        }
 315
 316        let filter = self.filter_text.to_lowercase();
 317        self.components
 318            .iter()
 319            .filter(|component| {
 320                let component_name = component.name().to_lowercase();
 321                let scope_name = component.scope().to_string().to_lowercase();
 322                let description = component
 323                    .description()
 324                    .map(|d| d.to_lowercase())
 325                    .unwrap_or_default();
 326
 327                component_name.contains(&filter)
 328                    || scope_name.contains(&filter)
 329                    || description.contains(&filter)
 330            })
 331            .cloned()
 332            .collect()
 333    }
 334
 335    fn scope_ordered_entries(&self) -> Vec<PreviewEntry> {
 336        use std::collections::HashMap;
 337
 338        let mut scope_groups: HashMap<
 339            ComponentScope,
 340            Vec<(ComponentMetadata, Option<Vec<usize>>)>,
 341        > = HashMap::default();
 342        let lowercase_filter = self.filter_text.to_lowercase();
 343
 344        for component in &self.components {
 345            if self.filter_text.is_empty() {
 346                scope_groups
 347                    .entry(component.scope())
 348                    .or_insert_with(Vec::new)
 349                    .push((component.clone(), None));
 350                continue;
 351            }
 352
 353            // let full_component_name = component.name();
 354            let scopeless_name = component.scopeless_name();
 355            let scope_name = component.scope().to_string();
 356            let description = component.description().unwrap_or_default();
 357
 358            let lowercase_scopeless = scopeless_name.to_lowercase();
 359            let lowercase_scope = scope_name.to_lowercase();
 360            let lowercase_desc = description.to_lowercase();
 361
 362            if lowercase_scopeless.contains(&lowercase_filter) {
 363                if let Some(index) = lowercase_scopeless.find(&lowercase_filter) {
 364                    let end = index + lowercase_filter.len();
 365
 366                    if end <= scopeless_name.len() {
 367                        let mut positions = Vec::new();
 368                        for i in index..end {
 369                            if scopeless_name.is_char_boundary(i) {
 370                                positions.push(i);
 371                            }
 372                        }
 373
 374                        if !positions.is_empty() {
 375                            scope_groups
 376                                .entry(component.scope())
 377                                .or_insert_with(Vec::new)
 378                                .push((component.clone(), Some(positions)));
 379                            continue;
 380                        }
 381                    }
 382                }
 383            }
 384
 385            if lowercase_scopeless.contains(&lowercase_filter)
 386                || lowercase_scope.contains(&lowercase_filter)
 387                || lowercase_desc.contains(&lowercase_filter)
 388            {
 389                scope_groups
 390                    .entry(component.scope())
 391                    .or_insert_with(Vec::new)
 392                    .push((component.clone(), None));
 393            }
 394        }
 395
 396        // Sort the components in each group
 397        for components in scope_groups.values_mut() {
 398            components.sort_by_key(|(c, _)| c.sort_name());
 399        }
 400
 401        let mut entries = Vec::new();
 402
 403        // Always show all components first
 404        entries.push(PreviewEntry::AllComponents);
 405        entries.push(PreviewEntry::ActiveThread);
 406        entries.push(PreviewEntry::Separator);
 407
 408        let mut scopes: Vec<_> = scope_groups
 409            .keys()
 410            .filter(|scope| !matches!(**scope, ComponentScope::None))
 411            .cloned()
 412            .collect();
 413
 414        scopes.sort_by_key(|s| s.to_string());
 415
 416        for scope in scopes {
 417            if let Some(components) = scope_groups.remove(&scope) {
 418                if !components.is_empty() {
 419                    entries.push(PreviewEntry::SectionHeader(scope.to_string().into()));
 420                    let mut sorted_components = components;
 421                    sorted_components.sort_by_key(|(component, _)| component.sort_name());
 422
 423                    for (component, positions) in sorted_components {
 424                        entries.push(PreviewEntry::Component(component, positions));
 425                    }
 426                }
 427            }
 428        }
 429
 430        // Add uncategorized components last
 431        if let Some(components) = scope_groups.get(&ComponentScope::None) {
 432            if !components.is_empty() {
 433                entries.push(PreviewEntry::Separator);
 434                entries.push(PreviewEntry::SectionHeader("Uncategorized".into()));
 435                let mut sorted_components = components.clone();
 436                sorted_components.sort_by_key(|(c, _)| c.sort_name());
 437
 438                for (component, positions) in sorted_components {
 439                    entries.push(PreviewEntry::Component(component, positions));
 440                }
 441            }
 442        }
 443
 444        entries
 445    }
 446
 447    fn render_sidebar_entry(
 448        &self,
 449        ix: usize,
 450        entry: &PreviewEntry,
 451        cx: &Context<Self>,
 452    ) -> impl IntoElement + use<> {
 453        match entry {
 454            PreviewEntry::Component(component_metadata, highlight_positions) => {
 455                let id = component_metadata.id();
 456                let selected = self.active_page == PreviewPage::Component(id.clone());
 457                let name = component_metadata.scopeless_name();
 458
 459                ListItem::new(ix)
 460                    .child(if let Some(_positions) = highlight_positions {
 461                        let name_lower = name.to_lowercase();
 462                        let filter_lower = self.filter_text.to_lowercase();
 463                        let valid_positions = if let Some(start) = name_lower.find(&filter_lower) {
 464                            let end = start + filter_lower.len();
 465                            (start..end).collect()
 466                        } else {
 467                            Vec::new()
 468                        };
 469                        if valid_positions.is_empty() {
 470                            Label::new(name.clone())
 471                                .color(Color::Default)
 472                                .into_any_element()
 473                        } else {
 474                            HighlightedLabel::new(name.clone(), valid_positions).into_any_element()
 475                        }
 476                    } else {
 477                        Label::new(name.clone())
 478                            .color(Color::Default)
 479                            .into_any_element()
 480                    })
 481                    .selectable(true)
 482                    .toggle_state(selected)
 483                    .inset(true)
 484                    .on_click(cx.listener(move |this, _, _, cx| {
 485                        let id = id.clone();
 486                        this.set_active_page(PreviewPage::Component(id), cx);
 487                    }))
 488                    .into_any_element()
 489            }
 490            PreviewEntry::SectionHeader(shared_string) => ListSubHeader::new(shared_string)
 491                .inset(true)
 492                .into_any_element(),
 493            PreviewEntry::AllComponents => {
 494                let selected = self.active_page == PreviewPage::AllComponents;
 495
 496                ListItem::new(ix)
 497                    .child(Label::new("All Components").color(Color::Default))
 498                    .selectable(true)
 499                    .toggle_state(selected)
 500                    .inset(true)
 501                    .on_click(cx.listener(move |this, _, _, cx| {
 502                        this.set_active_page(PreviewPage::AllComponents, cx);
 503                    }))
 504                    .into_any_element()
 505            }
 506            PreviewEntry::ActiveThread => {
 507                let selected = self.active_page == PreviewPage::ActiveThread;
 508
 509                ListItem::new(ix)
 510                    .child(Label::new("Active Thread").color(Color::Default))
 511                    .selectable(true)
 512                    .toggle_state(selected)
 513                    .inset(true)
 514                    .on_click(cx.listener(move |this, _, _, cx| {
 515                        this.set_active_page(PreviewPage::ActiveThread, cx);
 516                    }))
 517                    .into_any_element()
 518            }
 519            PreviewEntry::Separator => ListItem::new(ix)
 520                .child(
 521                    h_flex()
 522                        .occlude()
 523                        .pt_3()
 524                        .child(Divider::horizontal_dashed()),
 525                )
 526                .into_any_element(),
 527        }
 528    }
 529
 530    fn update_component_list(&mut self, cx: &mut Context<Self>) {
 531        let entries = self.scope_ordered_entries();
 532        let new_len = entries.len();
 533        let weak_entity = cx.entity().downgrade();
 534
 535        if new_len > 0 {
 536            self.nav_scroll_handle
 537                .scroll_to_item(0, ScrollStrategy::Top);
 538        }
 539
 540        let filtered_components = self.filtered_components();
 541
 542        if !self.filter_text.is_empty() && !matches!(self.active_page, PreviewPage::AllComponents) {
 543            if let PreviewPage::Component(ref component_id) = self.active_page {
 544                let component_still_visible = filtered_components
 545                    .iter()
 546                    .any(|component| component.id() == *component_id);
 547
 548                if !component_still_visible {
 549                    if !filtered_components.is_empty() {
 550                        let first_component = &filtered_components[0];
 551                        self.set_active_page(PreviewPage::Component(first_component.id()), cx);
 552                    } else {
 553                        self.set_active_page(PreviewPage::AllComponents, cx);
 554                    }
 555                }
 556            }
 557        }
 558
 559        self.component_list = ListState::new(
 560            filtered_components.len(),
 561            gpui::ListAlignment::Top,
 562            px(1500.0),
 563            {
 564                let components = filtered_components.clone();
 565                let this = cx.entity().downgrade();
 566                move |ix, window: &mut Window, cx: &mut App| {
 567                    if ix >= components.len() {
 568                        return div().w_full().h_0().into_any_element();
 569                    }
 570
 571                    this.update(cx, |this, cx| {
 572                        let component = &components[ix];
 573                        this.render_preview(component, window, cx)
 574                            .into_any_element()
 575                    })
 576                    .unwrap()
 577                }
 578            },
 579        );
 580
 581        let new_list = ListState::new(
 582            new_len,
 583            gpui::ListAlignment::Top,
 584            px(1500.0),
 585            move |ix, window, cx| {
 586                if ix >= entries.len() {
 587                    return div().w_full().h_0().into_any_element();
 588                }
 589
 590                let entry = &entries[ix];
 591
 592                weak_entity
 593                    .update(cx, |this, cx| match entry {
 594                        PreviewEntry::Component(component, _) => this
 595                            .render_preview(component, window, cx)
 596                            .into_any_element(),
 597                        PreviewEntry::SectionHeader(shared_string) => this
 598                            .render_scope_header(ix, shared_string.clone(), window, cx)
 599                            .into_any_element(),
 600                        PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
 601                        PreviewEntry::ActiveThread => div().w_full().h_0().into_any_element(),
 602                        PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
 603                    })
 604                    .unwrap()
 605            },
 606        );
 607
 608        self.component_list = new_list;
 609        cx.emit(ItemEvent::UpdateTab);
 610    }
 611
 612    fn render_scope_header(
 613        &self,
 614        _ix: usize,
 615        title: SharedString,
 616        _window: &Window,
 617        _cx: &App,
 618    ) -> impl IntoElement {
 619        h_flex()
 620            .w_full()
 621            .h_10()
 622            .items_center()
 623            .child(Headline::new(title).size(HeadlineSize::XSmall))
 624            .child(Divider::horizontal())
 625    }
 626
 627    fn render_preview(
 628        &self,
 629        component: &ComponentMetadata,
 630        window: &mut Window,
 631        cx: &mut App,
 632    ) -> impl IntoElement {
 633        let name = component.scopeless_name();
 634        let scope = component.scope();
 635
 636        let description = component.description();
 637
 638        v_flex()
 639            .py_2()
 640            .child(
 641                v_flex()
 642                    .border_1()
 643                    .border_color(cx.theme().colors().border)
 644                    .rounded_sm()
 645                    .w_full()
 646                    .gap_4()
 647                    .py_4()
 648                    .px_6()
 649                    .flex_none()
 650                    .child(
 651                        v_flex()
 652                            .gap_1()
 653                            .child(
 654                                h_flex().gap_1().text_xl().child(div().child(name)).when(
 655                                    !matches!(scope, ComponentScope::None),
 656                                    |this| {
 657                                        this.child(div().opacity(0.5).child(format!("({})", scope)))
 658                                    },
 659                                ),
 660                            )
 661                            .when_some(description, |this, description| {
 662                                this.child(
 663                                    div()
 664                                        .text_ui_sm(cx)
 665                                        .text_color(cx.theme().colors().text_muted)
 666                                        .max_w(px(600.0))
 667                                        .child(description),
 668                                )
 669                            }),
 670                    )
 671                    .when_some(component.preview(), |this, preview| {
 672                        this.children(preview(window, cx))
 673                    }),
 674            )
 675            .into_any_element()
 676    }
 677
 678    fn render_all_components(&self, cx: &Context<Self>) -> impl IntoElement {
 679        v_flex()
 680            .id("component-list")
 681            .px_8()
 682            .pt_4()
 683            .size_full()
 684            .child(
 685                if self.filtered_components().is_empty() && !self.filter_text.is_empty() {
 686                    div()
 687                        .size_full()
 688                        .items_center()
 689                        .justify_center()
 690                        .text_color(cx.theme().colors().text_muted)
 691                        .child(format!("No components matching '{}'.", self.filter_text))
 692                        .into_any_element()
 693                } else {
 694                    list(self.component_list.clone())
 695                        .flex_grow()
 696                        .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
 697                        .into_any_element()
 698                },
 699            )
 700    }
 701
 702    fn render_component_page(
 703        &mut self,
 704        component_id: &ComponentId,
 705        _window: &mut Window,
 706        _cx: &mut Context<Self>,
 707    ) -> impl IntoElement {
 708        let component = self.component_map.get(&component_id);
 709
 710        if let Some(component) = component {
 711            v_flex()
 712                .id("render-component-page")
 713                .size_full()
 714                .child(ComponentPreviewPage::new(component.clone()))
 715                .into_any_element()
 716        } else {
 717            v_flex()
 718                .size_full()
 719                .items_center()
 720                .justify_center()
 721                .child("Component not found")
 722                .into_any_element()
 723        }
 724    }
 725
 726    fn render_active_thread(
 727        &self,
 728        window: &mut Window,
 729        cx: &mut Context<Self>,
 730    ) -> impl IntoElement {
 731        v_flex()
 732            .id("render-active-thread")
 733            .size_full()
 734            .child(
 735                v_flex().children(self.agent_previews.iter().filter_map(|preview_fn| {
 736                    if let (Some(thread_store), Some(active_thread)) = (
 737                        self.thread_store.as_ref().map(|ts| ts.downgrade()),
 738                        self.active_thread.clone(),
 739                    ) {
 740                        preview_fn(
 741                            self,
 742                            self.workspace.clone(),
 743                            active_thread,
 744                            thread_store,
 745                            window,
 746                            cx,
 747                        )
 748                        .map(|element| div().child(element))
 749                    } else {
 750                        None
 751                    }
 752                })),
 753            )
 754            .children(self.active_thread.clone().map(|thread| thread.clone()))
 755            .when_none(&self.active_thread.clone(), |this| {
 756                this.child("No active thread")
 757            })
 758            .into_any_element()
 759    }
 760
 761    fn test_status_toast(&self, cx: &mut Context<Self>) {
 762        if let Some(workspace) = self.workspace.upgrade() {
 763            workspace.update(cx, |workspace, cx| {
 764                let status_toast =
 765                    StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
 766                        this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
 767                            .action("Open Pull Request", |_, cx| {
 768                                cx.open_url("https://github.com/")
 769                            })
 770                    });
 771                workspace.toggle_status_toast(status_toast, cx)
 772            });
 773        }
 774    }
 775}
 776
 777impl Render for ComponentPreview {
 778    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 779        // TODO: move this into the struct
 780        let current_filter = self.filter_editor.update(cx, |input, cx| {
 781            if input.is_empty(cx) {
 782                String::new()
 783            } else {
 784                input.editor().read(cx).text(cx).to_string()
 785            }
 786        });
 787
 788        if current_filter != self.filter_text {
 789            self.filter_text = current_filter;
 790            self.update_component_list(cx);
 791        }
 792        let sidebar_entries = self.scope_ordered_entries();
 793        let active_page = self.active_page.clone();
 794
 795        h_flex()
 796            .id("component-preview")
 797            .key_context("ComponentPreview")
 798            .items_start()
 799            .overflow_hidden()
 800            .size_full()
 801            .track_focus(&self.focus_handle)
 802            .bg(cx.theme().colors().editor_background)
 803            .child(
 804                v_flex()
 805                    .border_r_1()
 806                    .border_color(cx.theme().colors().border)
 807                    .h_full()
 808                    .child(
 809                        gpui::uniform_list(
 810                            cx.entity().clone(),
 811                            "component-nav",
 812                            sidebar_entries.len(),
 813                            move |this, range, _window, cx| {
 814                                range
 815                                    .filter_map(|ix| {
 816                                        if ix < sidebar_entries.len() {
 817                                            Some(this.render_sidebar_entry(
 818                                                ix,
 819                                                &sidebar_entries[ix],
 820                                                cx,
 821                                            ))
 822                                        } else {
 823                                            None
 824                                        }
 825                                    })
 826                                    .collect()
 827                            },
 828                        )
 829                        .track_scroll(self.nav_scroll_handle.clone())
 830                        .pt_4()
 831                        .px_4()
 832                        .w(px(240.))
 833                        .h_full()
 834                        .flex_1(),
 835                    )
 836                    .child(
 837                        div().w_full().pb_4().child(
 838                            Button::new("toast-test", "Launch Toast")
 839                                .on_click(cx.listener({
 840                                    move |this, _, _window, cx| {
 841                                        this.test_status_toast(cx);
 842                                        cx.notify();
 843                                    }
 844                                }))
 845                                .full_width(),
 846                        ),
 847                    ),
 848            )
 849            .child(
 850                v_flex()
 851                    .id("content-area")
 852                    .flex_1()
 853                    .size_full()
 854                    .overflow_hidden()
 855                    .child(
 856                        div()
 857                            .p_2()
 858                            .w_full()
 859                            .border_b_1()
 860                            .border_color(cx.theme().colors().border)
 861                            .child(self.filter_editor.clone()),
 862                    )
 863                    .child(match active_page {
 864                        PreviewPage::AllComponents => {
 865                            self.render_all_components(cx).into_any_element()
 866                        }
 867                        PreviewPage::Component(id) => self
 868                            .render_component_page(&id, window, cx)
 869                            .into_any_element(),
 870                        PreviewPage::ActiveThread => {
 871                            self.render_active_thread(window, cx).into_any_element()
 872                        }
 873                    }),
 874            )
 875    }
 876}
 877
 878impl EventEmitter<ItemEvent> for ComponentPreview {}
 879
 880impl Focusable for ComponentPreview {
 881    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
 882        self.focus_handle.clone()
 883    }
 884}
 885
 886#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 887pub struct ActivePageId(pub String);
 888
 889impl Default for ActivePageId {
 890    fn default() -> Self {
 891        ActivePageId("AllComponents".to_string())
 892    }
 893}
 894
 895impl From<ComponentId> for ActivePageId {
 896    fn from(id: ComponentId) -> Self {
 897        ActivePageId(id.0.to_string())
 898    }
 899}
 900
 901impl Item for ComponentPreview {
 902    type Event = ItemEvent;
 903
 904    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
 905        "Component Preview".into()
 906    }
 907
 908    fn telemetry_event_text(&self) -> Option<&'static str> {
 909        None
 910    }
 911
 912    fn show_toolbar(&self) -> bool {
 913        false
 914    }
 915
 916    fn clone_on_split(
 917        &self,
 918        _workspace_id: Option<WorkspaceId>,
 919        window: &mut Window,
 920        cx: &mut Context<Self>,
 921    ) -> Option<gpui::Entity<Self>>
 922    where
 923        Self: Sized,
 924    {
 925        let language_registry = self.language_registry.clone();
 926        let user_store = self.user_store.clone();
 927        let weak_workspace = self.workspace.clone();
 928        let project = self.project.clone();
 929        let selected_index = self.cursor_index;
 930        let active_page = self.active_page.clone();
 931
 932        let self_result = Self::new(
 933            weak_workspace,
 934            project,
 935            language_registry,
 936            user_store,
 937            selected_index,
 938            Some(active_page),
 939            window,
 940            cx,
 941        );
 942
 943        match self_result {
 944            Ok(preview) => Some(cx.new(|_cx| preview)),
 945            Err(e) => {
 946                log::error!("Failed to clone component preview: {}", e);
 947                None
 948            }
 949        }
 950    }
 951
 952    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
 953        f(*event)
 954    }
 955
 956    fn added_to_workspace(
 957        &mut self,
 958        workspace: &mut Workspace,
 959        window: &mut Window,
 960        cx: &mut Context<Self>,
 961    ) {
 962        self.workspace_id = workspace.database_id();
 963
 964        let focus_handle = self.filter_editor.read(cx).focus_handle(cx);
 965        window.focus(&focus_handle);
 966    }
 967}
 968
 969impl SerializableItem for ComponentPreview {
 970    fn serialized_item_kind() -> &'static str {
 971        "ComponentPreview"
 972    }
 973
 974    fn deserialize(
 975        project: Entity<Project>,
 976        workspace: WeakEntity<Workspace>,
 977        workspace_id: WorkspaceId,
 978        item_id: ItemId,
 979        window: &mut Window,
 980        cx: &mut App,
 981    ) -> Task<gpui::Result<Entity<Self>>> {
 982        let deserialized_active_page =
 983            match COMPONENT_PREVIEW_DB.get_active_page(item_id, workspace_id) {
 984                Ok(page) => {
 985                    if let Some(page) = page {
 986                        ActivePageId(page)
 987                    } else {
 988                        ActivePageId::default()
 989                    }
 990                }
 991                Err(_) => ActivePageId::default(),
 992            };
 993
 994        let user_store = project.read(cx).user_store().clone();
 995        let language_registry = project.read(cx).languages().clone();
 996        let preview_page = if deserialized_active_page.0 == ActivePageId::default().0 {
 997            Some(PreviewPage::default())
 998        } else {
 999            let component_str = deserialized_active_page.0;
1000            let component_registry = components();
1001            let all_components = component_registry.all();
1002            let found_component = all_components.iter().find(|c| c.id().0 == component_str);
1003
1004            if let Some(component) = found_component {
1005                Some(PreviewPage::Component(component.id().clone()))
1006            } else {
1007                Some(PreviewPage::default())
1008            }
1009        };
1010
1011        window.spawn(cx, async move |cx| {
1012            let user_store = user_store.clone();
1013            let language_registry = language_registry.clone();
1014            let weak_workspace = workspace.clone();
1015            let project = project.clone();
1016            cx.update(move |window, cx| {
1017                Ok(cx.new(|cx| {
1018                    ComponentPreview::new(
1019                        weak_workspace,
1020                        project,
1021                        language_registry,
1022                        user_store,
1023                        None,
1024                        preview_page,
1025                        window,
1026                        cx,
1027                    )
1028                    .expect("Failed to create component preview")
1029                }))
1030            })?
1031        })
1032    }
1033
1034    fn cleanup(
1035        workspace_id: WorkspaceId,
1036        alive_items: Vec<ItemId>,
1037        _window: &mut Window,
1038        cx: &mut App,
1039    ) -> Task<gpui::Result<()>> {
1040        delete_unloaded_items(
1041            alive_items,
1042            workspace_id,
1043            "component_previews",
1044            &COMPONENT_PREVIEW_DB,
1045            cx,
1046        )
1047    }
1048
1049    fn serialize(
1050        &mut self,
1051        _workspace: &mut Workspace,
1052        item_id: ItemId,
1053        _closing: bool,
1054        _window: &mut Window,
1055        cx: &mut Context<Self>,
1056    ) -> Option<Task<gpui::Result<()>>> {
1057        let active_page = self.active_page_id(cx);
1058        let workspace_id = self.workspace_id?;
1059        Some(cx.background_spawn(async move {
1060            COMPONENT_PREVIEW_DB
1061                .save_active_page(item_id, workspace_id, active_page.0)
1062                .await
1063        }))
1064    }
1065
1066    fn should_serialize(&self, event: &Self::Event) -> bool {
1067        matches!(event, ItemEvent::UpdateTab)
1068    }
1069}
1070
1071// TODO: use language registry to allow rendering markdown
1072#[derive(IntoElement)]
1073pub struct ComponentPreviewPage {
1074    // languages: Arc<LanguageRegistry>,
1075    component: ComponentMetadata,
1076}
1077
1078impl ComponentPreviewPage {
1079    pub fn new(
1080        component: ComponentMetadata,
1081        // languages: Arc<LanguageRegistry>
1082    ) -> Self {
1083        Self {
1084            // languages,
1085            component,
1086        }
1087    }
1088
1089    fn render_header(&self, _: &Window, cx: &App) -> impl IntoElement {
1090        v_flex()
1091            .px_12()
1092            .pt_16()
1093            .pb_12()
1094            .gap_6()
1095            .bg(cx.theme().colors().surface_background)
1096            .border_b_1()
1097            .border_color(cx.theme().colors().border)
1098            .child(
1099                v_flex()
1100                    .gap_0p5()
1101                    .child(
1102                        Label::new(self.component.scope().to_string())
1103                            .size(LabelSize::Small)
1104                            .color(Color::Muted),
1105                    )
1106                    .child(
1107                        Headline::new(self.component.scopeless_name()).size(HeadlineSize::XLarge),
1108                    ),
1109            )
1110            .when_some(self.component.description(), |this, description| {
1111                this.child(div().text_sm().child(description))
1112            })
1113    }
1114
1115    fn render_preview(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1116        v_flex()
1117            .flex_1()
1118            .px_12()
1119            .py_6()
1120            .bg(cx.theme().colors().editor_background)
1121            .child(if let Some(preview) = self.component.preview() {
1122                preview(window, cx).unwrap_or_else(|| {
1123                    div()
1124                        .child("Failed to load preview. This path should be unreachable")
1125                        .into_any_element()
1126                })
1127            } else {
1128                div().child("No preview available").into_any_element()
1129            })
1130    }
1131}
1132
1133impl RenderOnce for ComponentPreviewPage {
1134    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1135        v_flex()
1136            .id("component-preview-page")
1137            .overflow_y_scroll()
1138            .overflow_x_hidden()
1139            .w_full()
1140            .child(self.render_header(window, cx))
1141            .child(self.render_preview(window, cx))
1142    }
1143}