1001 lines · 34.4 KB · Rust
RawDownload
mod component_preview_example;mod persistence; use client::UserStore;use collections::HashMap;use component::{ComponentId, ComponentMetadata, ComponentStatus, components};use gpui::{    App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, list, prelude::*,};use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle};use language::LanguageRegistry;use notifications::status_toast::{StatusToast, ToastIcon};use persistence::COMPONENT_PREVIEW_DB;use project::Project;use std::{iter::Iterator, ops::Range, sync::Arc};use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*};use ui_input::InputField;use workspace::AppState;use workspace::{    Item, ItemId, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items, item::ItemEvent,}; #[allow(unused_imports)]pub use component_preview_example::*; pub fn init(app_state: Arc<AppState>, cx: &mut App) {    workspace::register_serializable_item::<ComponentPreview>(cx);     cx.observe_new(move |workspace: &mut Workspace, _window, cx| {        let app_state = app_state.clone();        let project = workspace.project().clone();        let weak_workspace = cx.entity().downgrade();         workspace.register_action(            move |workspace, _: &workspace::OpenComponentPreview, window, cx| {                let app_state = app_state.clone();                 let language_registry = app_state.languages.clone();                let user_store = app_state.user_store.clone();                 let component_preview = cx.new(|cx| {                    ComponentPreview::new(                        weak_workspace.clone(),                        project.clone(),                        language_registry,                        user_store,                        None,                        None,                        window,                        cx,                    )                    .expect("Failed to create component preview")                });                 workspace.add_item_to_active_pane(                    Box::new(component_preview),                    None,                    true,                    window,                    cx,                )            },        );    })    .detach();} enum PreviewEntry {    AllComponents,    Separator,    Component(ComponentMetadata, Option<Vec<usize>>),    SectionHeader(SharedString),} impl From<ComponentMetadata> for PreviewEntry {    fn from(component: ComponentMetadata) -> Self {        PreviewEntry::Component(component, None)    }} impl From<SharedString> for PreviewEntry {    fn from(section_header: SharedString) -> Self {        PreviewEntry::SectionHeader(section_header)    }} #[derive(Default, Debug, Clone, PartialEq, Eq)]enum PreviewPage {    #[default]    AllComponents,    Component(ComponentId),} struct ComponentPreview {    active_page: PreviewPage,    reset_key: usize,    component_list: ListState,    entries: Vec<PreviewEntry>,    component_map: HashMap<ComponentId, ComponentMetadata>,    components: Vec<ComponentMetadata>,    cursor_index: usize,    filter_editor: Entity<InputField>,    filter_text: String,    focus_handle: FocusHandle,    language_registry: Arc<LanguageRegistry>,    nav_scroll_handle: UniformListScrollHandle,    project: Entity<Project>,    user_store: Entity<UserStore>,    workspace: WeakEntity<Workspace>,    workspace_id: Option<WorkspaceId>,    _view_scroll_handle: ScrollHandle,} impl ComponentPreview {    pub fn new(        workspace: WeakEntity<Workspace>,        project: Entity<Project>,        language_registry: Arc<LanguageRegistry>,        user_store: Entity<UserStore>,        selected_index: impl Into<Option<usize>>,        active_page: Option<PreviewPage>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> anyhow::Result<Self> {        let component_registry = Arc::new(components());        let sorted_components = component_registry.sorted_components();        let selected_index = selected_index.into().unwrap_or(0);        let active_page = active_page.unwrap_or(PreviewPage::AllComponents);        let filter_editor = cx.new(|cx| InputField::new(window, cx, "Find components or usages…"));         let component_list = ListState::new(            sorted_components.len(),            gpui::ListAlignment::Top,            px(1500.0),        );         let mut component_preview = Self {            active_page,            reset_key: 0,            component_list,            entries: Vec::new(),            component_map: component_registry.component_map(),            components: sorted_components,            cursor_index: selected_index,            filter_editor,            filter_text: String::new(),            focus_handle: cx.focus_handle(),            language_registry,            nav_scroll_handle: UniformListScrollHandle::new(),            project,            user_store,            workspace,            workspace_id: None,            _view_scroll_handle: ScrollHandle::new(),        };         if component_preview.cursor_index > 0 {            component_preview.scroll_to_preview(component_preview.cursor_index, cx);        }         component_preview.update_component_list(cx);         let focus_handle = component_preview.filter_editor.read(cx).focus_handle(cx);        window.focus(&focus_handle, cx);         Ok(component_preview)    }     pub fn active_page_id(&self, _cx: &App) -> ActivePageId {        match &self.active_page {            PreviewPage::AllComponents => ActivePageId::default(),            PreviewPage::Component(component_id) => ActivePageId(component_id.0.to_string()),        }    }     fn scroll_to_preview(&mut self, ix: usize, cx: &mut Context<Self>) {        self.component_list.scroll_to_reveal_item(ix);        self.cursor_index = ix;        cx.notify();    }     fn set_active_page(&mut self, page: PreviewPage, cx: &mut Context<Self>) {        if self.active_page == page {            // Force the current preview page to render again            self.reset_key = self.reset_key.wrapping_add(1);        } else {            self.active_page = page;            cx.emit(ItemEvent::UpdateTab);        }        cx.notify();    }     fn filtered_components(&self) -> Vec<ComponentMetadata> {        if self.filter_text.is_empty() {            return self.components.clone();        }         let filter = self.filter_text.to_lowercase();        self.components            .iter()            .filter(|component| {                let component_name = component.name().to_lowercase();                let scope_name = component.scope().to_string().to_lowercase();                let description = component                    .description()                    .map(|d| d.to_lowercase())                    .unwrap_or_default();                 component_name.contains(&filter)                    || scope_name.contains(&filter)                    || description.contains(&filter)            })            .cloned()            .collect()    }     fn scope_ordered_entries(&self) -> Vec<PreviewEntry> {        use collections::HashMap;         let mut scope_groups: HashMap<            ComponentScope,            Vec<(ComponentMetadata, Option<Vec<usize>>)>,        > = HashMap::default();        let lowercase_filter = self.filter_text.to_lowercase();         for component in &self.components {            if self.filter_text.is_empty() {                scope_groups                    .entry(component.scope())                    .or_insert_with(Vec::new)                    .push((component.clone(), None));                continue;            }             // let full_component_name = component.name();            let scopeless_name = component.scopeless_name();            let scope_name = component.scope().to_string();            let description = component.description().unwrap_or_default();             let lowercase_scopeless = scopeless_name.to_lowercase();            let lowercase_scope = scope_name.to_lowercase();            let lowercase_desc = description.to_lowercase();             if lowercase_scopeless.contains(&lowercase_filter)                && let Some(index) = lowercase_scopeless.find(&lowercase_filter)            {                let end = index + lowercase_filter.len();                 if end <= scopeless_name.len() {                    let mut positions = Vec::new();                    for i in index..end {                        if scopeless_name.is_char_boundary(i) {                            positions.push(i);                        }                    }                     if !positions.is_empty() {                        scope_groups                            .entry(component.scope())                            .or_insert_with(Vec::new)                            .push((component.clone(), Some(positions)));                        continue;                    }                }            }             if lowercase_scopeless.contains(&lowercase_filter)                || lowercase_scope.contains(&lowercase_filter)                || lowercase_desc.contains(&lowercase_filter)            {                scope_groups                    .entry(component.scope())                    .or_insert_with(Vec::new)                    .push((component.clone(), None));            }        }         // Sort the components in each group        for components in scope_groups.values_mut() {            components.sort_by_key(|(c, _)| c.sort_name());        }         let mut entries = Vec::new();         // Always show all components first        entries.push(PreviewEntry::AllComponents);         let mut scopes: Vec<_> = scope_groups            .keys()            .filter(|scope| !matches!(**scope, ComponentScope::None))            .cloned()            .collect();         scopes.sort_by_key(|s| s.to_string());         for scope in scopes {            if let Some(components) = scope_groups.remove(&scope)                && !components.is_empty()            {                entries.push(PreviewEntry::Separator);                entries.push(PreviewEntry::SectionHeader(scope.to_string().into()));                 let mut sorted_components = components;                sorted_components.sort_by_key(|(component, _)| component.sort_name());                 for (component, positions) in sorted_components {                    entries.push(PreviewEntry::Component(component, positions));                }            }        }         // Add uncategorized components last        if let Some(components) = scope_groups.get(&ComponentScope::None)            && !components.is_empty()        {            entries.push(PreviewEntry::Separator);            entries.push(PreviewEntry::SectionHeader("Uncategorized".into()));            let mut sorted_components = components.clone();            sorted_components.sort_by_key(|(c, _)| c.sort_name());             for (component, positions) in sorted_components {                entries.push(PreviewEntry::Component(component, positions));            }        }         entries    }     fn update_component_list(&mut self, cx: &mut Context<Self>) {        let entries = self.scope_ordered_entries();        let new_len = entries.len();         if new_len > 0 {            self.nav_scroll_handle                .scroll_to_item(0, ScrollStrategy::Top);        }         let filtered_components = self.filtered_components();         if !self.filter_text.is_empty()            && !matches!(self.active_page, PreviewPage::AllComponents)            && let PreviewPage::Component(ref component_id) = self.active_page        {            let component_still_visible = filtered_components                .iter()                .any(|component| component.id() == *component_id);             if !component_still_visible {                if !filtered_components.is_empty() {                    let first_component = &filtered_components[0];                    self.set_active_page(PreviewPage::Component(first_component.id()), cx);                } else {                    self.set_active_page(PreviewPage::AllComponents, cx);                }            }        }         self.component_list = ListState::new(new_len, gpui::ListAlignment::Top, px(1500.0));        self.entries = entries;         cx.emit(ItemEvent::UpdateTab);    }     fn render_sidebar_entry(        &self,        ix: usize,        entry: &PreviewEntry,        cx: &Context<Self>,    ) -> impl IntoElement + use<> {        match entry {            PreviewEntry::Component(component_metadata, highlight_positions) => {                let id = component_metadata.id();                let selected = self.active_page == PreviewPage::Component(id.clone());                let name = component_metadata.scopeless_name();                 ListItem::new(ix)                    .child(if let Some(_positions) = highlight_positions {                        let name_lower = name.to_lowercase();                        let filter_lower = self.filter_text.to_lowercase();                        let valid_positions = if let Some(start) = name_lower.find(&filter_lower) {                            let end = start + filter_lower.len();                            (start..end).collect()                        } else {                            Vec::new()                        };                        if valid_positions.is_empty() {                            Label::new(name).into_any_element()                        } else {                            HighlightedLabel::new(name, valid_positions).into_any_element()                        }                    } else {                        Label::new(name).into_any_element()                    })                    .selectable(true)                    .toggle_state(selected)                    .inset(true)                    .on_click(cx.listener(move |this, _, _, cx| {                        let id = id.clone();                        this.set_active_page(PreviewPage::Component(id), cx);                    }))                    .into_any_element()            }            PreviewEntry::SectionHeader(shared_string) => ListSubHeader::new(shared_string)                .inset(true)                .into_any_element(),            PreviewEntry::AllComponents => {                let selected = self.active_page == PreviewPage::AllComponents;                 ListItem::new(ix)                    .child(Label::new("All Components"))                    .selectable(true)                    .toggle_state(selected)                    .inset(true)                    .on_click(cx.listener(move |this, _, _, cx| {                        this.set_active_page(PreviewPage::AllComponents, cx);                    }))                    .into_any_element()            }            PreviewEntry::Separator => ListItem::new(ix)                .disabled(true)                .child(div().w_full().py_2().child(Divider::horizontal()))                .into_any_element(),        }    }     fn render_scope_header(        &self,        _ix: usize,        title: SharedString,        _window: &Window,        _cx: &App,    ) -> impl IntoElement {        h_flex()            .w_full()            .h_10()            .child(Headline::new(title).size(HeadlineSize::XSmall))            .child(Divider::horizontal())    }     fn render_preview(        &self,        component: &ComponentMetadata,        window: &mut Window,        cx: &mut App,    ) -> impl IntoElement {        let name = component.scopeless_name();        let scope = component.scope();         let description = component.description();         // Build the content container        let mut preview_container = v_flex().py_2().child(            v_flex()                .border_1()                .border_color(cx.theme().colors().border)                .rounded_sm()                .w_full()                .gap_4()                .py_4()                .px_6()                .flex_none()                .child(                    v_flex()                        .gap_1()                        .child(                            h_flex()                                .gap_1()                                .text_xl()                                .child(div().child(name))                                .when(!matches!(scope, ComponentScope::None), |this| {                                    this.child(div().opacity(0.5).child(format!("({})", scope)))                                }),                        )                        .when_some(description, |this, description| {                            this.child(                                div()                                    .text_ui_sm(cx)                                    .text_color(cx.theme().colors().text_muted)                                    .max_w(px(600.0))                                    .child(description),                            )                        }),                ),        );         if let Some(preview) = component.preview() {            preview_container = preview_container.children(preview(window, cx));        }         preview_container.into_any_element()    }     fn render_all_components(&self, cx: &Context<Self>) -> impl IntoElement {        v_flex()            .id("component-list")            .px_8()            .pt_4()            .size_full()            .child(                if self.filtered_components().is_empty() && !self.filter_text.is_empty() {

Lines 1–500 of 1001.