component: Component crate cleanup (#29967)

This PR further organizes and documents the component crate. It:

- Simplifies the component registry
- Gives access to `ComponentMetadata` sooner
- Enables lookup by id in preview extension implementations
(`ComponentId` -> `ComponentMetadata`)
- Should slightly improve the performance of ComponentPreview

It also brings component statuses to the Component trait:

![CleanShot 2025-05-05 at 23 27
11@2x](https://github.com/user-attachments/assets/dd95ede6-bc90-4de4-90c6-3e5e064fd676)

![CleanShot 2025-05-05 at 23 27
40@2x](https://github.com/user-attachments/assets/9520aece-04c2-418b-95e1-c11aa60a66ca)

![CleanShot 2025-05-05 at 23 27
57@2x](https://github.com/user-attachments/assets/db1713d5-9831-4d00-9b29-1fd51c25fcba)

Release Notes:

- N/A
This commit is contained in:
Nate Butler
2025-05-06 03:41:52 +00:00
committed by GitHub
parent 377909a646
commit c5d8407df4
7 changed files with 627 additions and 500 deletions
+159 -115
View File
@@ -5,12 +5,13 @@
mod persistence;
mod preview_support;
use std::iter::Iterator;
use std::sync::Arc;
use std::iter::Iterator;
use agent::{ActiveThread, TextThreadStore, ThreadStore};
use client::UserStore;
use component::{ComponentId, ComponentMetadata, components};
use component::{ComponentId, ComponentMetadata, ComponentStatus, components};
use gpui::{
App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, list, prelude::*,
};
@@ -25,7 +26,7 @@ use preview_support::active_thread::{
load_preview_text_thread_store, load_preview_thread_store, static_active_thread,
};
use project::Project;
use ui::{Divider, HighlightedLabel, ListItem, ListSubHeader, prelude::*};
use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*};
use ui_input::SingleLineInput;
use util::ResultExt as _;
use workspace::{AppState, ItemId, SerializableItem, delete_unloaded_items};
@@ -104,26 +105,24 @@ enum PreviewPage {
}
struct ComponentPreview {
workspace_id: Option<WorkspaceId>,
focus_handle: FocusHandle,
_view_scroll_handle: ScrollHandle,
nav_scroll_handle: UniformListScrollHandle,
component_map: HashMap<ComponentId, ComponentMetadata>,
active_page: PreviewPage,
components: Vec<ComponentMetadata>,
active_thread: Option<Entity<ActiveThread>>,
component_list: ListState,
component_map: HashMap<ComponentId, ComponentMetadata>,
components: Vec<ComponentMetadata>,
cursor_index: usize,
language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
user_store: Entity<UserStore>,
filter_editor: Entity<SingleLineInput>,
filter_text: String,
// preview support
thread_store: Option<Entity<ThreadStore>>,
focus_handle: FocusHandle,
language_registry: Arc<LanguageRegistry>,
nav_scroll_handle: UniformListScrollHandle,
project: Entity<Project>,
text_thread_store: Option<Entity<TextThreadStore>>,
active_thread: Option<Entity<ActiveThread>>,
thread_store: Option<Entity<ThreadStore>>,
user_store: Entity<UserStore>,
workspace: WeakEntity<Workspace>,
workspace_id: Option<WorkspaceId>,
_view_scroll_handle: ScrollHandle,
}
impl ComponentPreview {
@@ -164,7 +163,8 @@ impl ComponentPreview {
})
.detach();
let sorted_components = components().all_sorted();
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 =
@@ -188,24 +188,24 @@ impl ComponentPreview {
);
let mut component_preview = Self {
workspace_id: None,
focus_handle: cx.focus_handle(),
_view_scroll_handle: ScrollHandle::new(),
nav_scroll_handle: UniformListScrollHandle::new(),
language_registry,
user_store,
workspace,
project,
active_page,
component_map: components().0,
components: sorted_components,
active_thread: None,
component_list,
component_map: component_registry.component_map(),
components: sorted_components,
cursor_index: selected_index,
filter_editor,
filter_text: String::new(),
thread_store: None,
focus_handle: cx.focus_handle(),
language_registry,
nav_scroll_handle: UniformListScrollHandle::new(),
project,
text_thread_store: None,
active_thread: None,
thread_store: None,
user_store,
workspace,
workspace_id: None,
_view_scroll_handle: ScrollHandle::new(),
};
if component_preview.cursor_index > 0 {
@@ -412,6 +412,88 @@ impl ComponentPreview {
entries
}
fn update_component_list(&mut self, cx: &mut Context<Self>) {
let entries = self.scope_ordered_entries();
let new_len = entries.len();
let weak_entity = cx.entity().downgrade();
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) {
if 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(
filtered_components.len(),
gpui::ListAlignment::Top,
px(1500.0),
{
let components = filtered_components.clone();
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
if ix >= components.len() {
return div().w_full().h_0().into_any_element();
}
this.update(cx, |this, cx| {
let component = &components[ix];
this.render_preview(component, window, cx)
.into_any_element()
})
.unwrap()
}
},
);
let new_list = ListState::new(
new_len,
gpui::ListAlignment::Top,
px(1500.0),
move |ix, window, cx| {
if ix >= entries.len() {
return div().w_full().h_0().into_any_element();
}
let entry = &entries[ix];
weak_entity
.update(cx, |this, cx| match entry {
PreviewEntry::Component(component, _) => this
.render_preview(component, window, cx)
.into_any_element(),
PreviewEntry::SectionHeader(shared_string) => this
.render_scope_header(ix, shared_string.clone(), window, cx)
.into_any_element(),
PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
PreviewEntry::ActiveThread => div().w_full().h_0().into_any_element(),
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
})
.unwrap()
},
);
self.component_list = new_list;
cx.emit(ItemEvent::UpdateTab);
}
fn render_sidebar_entry(
&self,
ix: usize,
@@ -495,88 +577,6 @@ impl ComponentPreview {
}
}
fn update_component_list(&mut self, cx: &mut Context<Self>) {
let entries = self.scope_ordered_entries();
let new_len = entries.len();
let weak_entity = cx.entity().downgrade();
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) {
if 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(
filtered_components.len(),
gpui::ListAlignment::Top,
px(1500.0),
{
let components = filtered_components.clone();
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
if ix >= components.len() {
return div().w_full().h_0().into_any_element();
}
this.update(cx, |this, cx| {
let component = &components[ix];
this.render_preview(component, window, cx)
.into_any_element()
})
.unwrap()
}
},
);
let new_list = ListState::new(
new_len,
gpui::ListAlignment::Top,
px(1500.0),
move |ix, window, cx| {
if ix >= entries.len() {
return div().w_full().h_0().into_any_element();
}
let entry = &entries[ix];
weak_entity
.update(cx, |this, cx| match entry {
PreviewEntry::Component(component, _) => this
.render_preview(component, window, cx)
.into_any_element(),
PreviewEntry::SectionHeader(shared_string) => this
.render_scope_header(ix, shared_string.clone(), window, cx)
.into_any_element(),
PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
PreviewEntry::ActiveThread => div().w_full().h_0().into_any_element(),
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
})
.unwrap()
},
);
self.component_list = new_list;
cx.emit(ItemEvent::UpdateTab);
}
fn render_scope_header(
&self,
_ix: usize,
@@ -695,7 +695,7 @@ impl ComponentPreview {
if let Some(component) = component {
v_flex()
.id("render-component-page")
.size_full()
.flex_1()
.child(ComponentPreviewPage::new(
component.clone(),
self.workspace.clone(),
@@ -971,7 +971,7 @@ impl SerializableItem for ComponentPreview {
} else {
let component_str = deserialized_active_page.0;
let component_registry = components();
let all_components = component_registry.all();
let all_components = component_registry.components();
let found_component = all_components.iter().find(|c| c.id().0 == component_str);
if let Some(component) = found_component {
@@ -1065,6 +1065,43 @@ impl ComponentPreviewPage {
}
}
/// Renders the component status when it would be useful
///
/// Doesn't render if the component is `ComponentStatus::Live`
/// as that is the default state
fn render_component_status(&self, cx: &App) -> Option<impl IntoElement> {
let status = self.component.status();
let status_description = status.description().to_string();
let color = match status {
ComponentStatus::Deprecated => Color::Error,
ComponentStatus::EngineeringReady => Color::Info,
ComponentStatus::Live => Color::Success,
ComponentStatus::WorkInProgress => Color::Warning,
};
if status != ComponentStatus::Live {
Some(
ButtonLike::new("component_status")
.child(
div()
.px_1p5()
.rounded_sm()
.bg(color.color(cx).alpha(0.12))
.child(
Label::new(status.clone().to_string())
.size(LabelSize::Small)
.color(color),
),
)
.tooltip(Tooltip::text(status_description))
.disabled(true),
)
} else {
None
}
}
fn render_header(&self, _: &Window, cx: &App) -> impl IntoElement {
v_flex()
.px_12()
@@ -1083,7 +1120,14 @@ impl ComponentPreviewPage {
.color(Color::Muted),
)
.child(
Headline::new(self.component.scopeless_name()).size(HeadlineSize::XLarge),
h_flex()
.items_center()
.gap_2()
.child(
Headline::new(self.component.scopeless_name())
.size(HeadlineSize::XLarge),
)
.children(self.render_component_status(cx)),
),
)
.when_some(self.component.description(), |this, description| {