Initial commit
This commit is contained in:
593
commands/gpui-component.md
Normal file
593
commands/gpui-component.md
Normal file
@@ -0,0 +1,593 @@
|
||||
---
|
||||
name: gpui-component
|
||||
description: Generate reusable GPUI components with proper typing, state management, styling, and documentation
|
||||
---
|
||||
|
||||
# GPUI Component Generator
|
||||
|
||||
Generate reusable, well-structured GPUI components with proper typing, state management, styling, and comprehensive documentation.
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$1`: Component name (required) - Name of the component in PascalCase (e.g., "Button", "DataTable", "SearchInput")
|
||||
- `$2`: Component type (optional) - Either "stateless" (default) or "stateful"
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Gather Component Requirements
|
||||
|
||||
Ask user for:
|
||||
- Component purpose and description
|
||||
- Props/configuration needed
|
||||
- Whether component needs internal state
|
||||
- Event handlers required
|
||||
- Styling requirements
|
||||
- Accessibility needs
|
||||
|
||||
### 2. Generate Component Struct
|
||||
|
||||
Create component struct based on type:
|
||||
|
||||
#### Stateless Component
|
||||
|
||||
```rust
|
||||
use gpui::*;
|
||||
|
||||
/// A reusable button component
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// Button::new("Click me")
|
||||
/// .on_click(|cx| {
|
||||
/// println!("Clicked!");
|
||||
/// })
|
||||
/// ```
|
||||
pub struct Button {
|
||||
label: String,
|
||||
variant: ButtonVariant,
|
||||
disabled: bool,
|
||||
on_click: Option<Box<dyn Fn(&mut WindowContext)>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum ButtonVariant {
|
||||
Primary,
|
||||
Secondary,
|
||||
Destructive,
|
||||
Ghost,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(label: impl Into<String>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
variant: ButtonVariant::Primary,
|
||||
disabled: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn variant(mut self, variant: ButtonVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(mut self, handler: impl Fn(&mut WindowContext) + 'static) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Stateful Component
|
||||
|
||||
```rust
|
||||
use gpui::*;
|
||||
|
||||
/// A search input with autocomplete
|
||||
pub struct SearchInput {
|
||||
query: String,
|
||||
suggestions: Vec<String>,
|
||||
selected_index: Option<usize>,
|
||||
on_search: Option<Box<dyn Fn(&str, &mut WindowContext)>>,
|
||||
}
|
||||
|
||||
impl SearchInput {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
query: String::new(),
|
||||
suggestions: Vec::new(),
|
||||
selected_index: None,
|
||||
on_search: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_search(mut self, handler: impl Fn(&str, &mut WindowContext) + 'static) -> Self {
|
||||
self.on_search = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, value: String, cx: &mut ViewContext<Self>) {
|
||||
self.query = value;
|
||||
self.update_suggestions(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn update_suggestions(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// Update suggestions based on query
|
||||
if let Some(handler) = &self.on_search {
|
||||
handler(&self.query, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) {
|
||||
match event.key.as_str() {
|
||||
"ArrowDown" => {
|
||||
self.selected_index = Some(
|
||||
self.selected_index
|
||||
.map(|i| (i + 1).min(self.suggestions.len() - 1))
|
||||
.unwrap_or(0)
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
"ArrowUp" => {
|
||||
self.selected_index = self.selected_index
|
||||
.and_then(|i| i.checked_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
"Enter" => {
|
||||
if let Some(index) = self.selected_index {
|
||||
self.query = self.suggestions[index].clone();
|
||||
self.suggestions.clear();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Implement Element/Render Traits
|
||||
|
||||
#### Stateless Component Render
|
||||
|
||||
```rust
|
||||
impl Render for Button {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let theme = cx.global::<Theme>();
|
||||
let (bg_color, text_color, hover_color) = match self.variant {
|
||||
ButtonVariant::Primary => (
|
||||
theme.primary,
|
||||
theme.primary_foreground,
|
||||
theme.primary_hover,
|
||||
),
|
||||
ButtonVariant::Secondary => (
|
||||
theme.secondary,
|
||||
theme.secondary_foreground,
|
||||
theme.secondary_hover,
|
||||
),
|
||||
ButtonVariant::Destructive => (
|
||||
theme.destructive,
|
||||
theme.destructive_foreground,
|
||||
theme.destructive_hover,
|
||||
),
|
||||
ButtonVariant::Ghost => (
|
||||
hsla(0.0, 0.0, 0.0, 0.0),
|
||||
theme.foreground,
|
||||
theme.muted,
|
||||
),
|
||||
};
|
||||
|
||||
div()
|
||||
.px_4()
|
||||
.py_2()
|
||||
.bg(bg_color)
|
||||
.text_color(text_color)
|
||||
.rounded_md()
|
||||
.font_medium()
|
||||
.when(!self.disabled, |this| {
|
||||
this.cursor_pointer()
|
||||
.hover(|this| this.bg(hover_color))
|
||||
})
|
||||
.when(self.disabled, |this| {
|
||||
this.opacity(0.5)
|
||||
.cursor_not_allowed()
|
||||
})
|
||||
.when_some(self.on_click.take(), |this, handler| {
|
||||
this.on_click(move |_, cx| {
|
||||
if !self.disabled {
|
||||
handler(cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(self.label.clone())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Stateful Component Render
|
||||
|
||||
```rust
|
||||
impl Render for SearchInput {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let theme = cx.global::<Theme>();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.bg(theme.background)
|
||||
.border_1()
|
||||
.border_color(theme.border)
|
||||
.rounded_md()
|
||||
.child(
|
||||
input()
|
||||
.flex_1()
|
||||
.placeholder("Search...")
|
||||
.value(&self.query)
|
||||
.on_input(cx.listener(|this, value, cx| {
|
||||
this.handle_input(value, cx);
|
||||
}))
|
||||
.on_key_down(cx.listener(|this, event, cx| {
|
||||
this.handle_key_down(event, cx);
|
||||
}))
|
||||
)
|
||||
)
|
||||
.when(!self.suggestions.is_empty(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_full()
|
||||
.left_0()
|
||||
.right_0()
|
||||
.mt_1()
|
||||
.bg(theme.background)
|
||||
.border_1()
|
||||
.border_color(theme.border)
|
||||
.rounded_md()
|
||||
.shadow_lg()
|
||||
.max_h_64()
|
||||
.overflow_y_auto()
|
||||
.children(
|
||||
self.suggestions.iter().enumerate().map(|(i, suggestion)| {
|
||||
div()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.cursor_pointer()
|
||||
.when(self.selected_index == Some(i), |this| {
|
||||
this.bg(theme.accent)
|
||||
})
|
||||
.hover(|this| this.bg(theme.muted))
|
||||
.child(suggestion.as_str())
|
||||
})
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Add State Management
|
||||
|
||||
For stateful components:
|
||||
|
||||
```rust
|
||||
impl SearchInput {
|
||||
pub fn set_suggestions(&mut self, suggestions: Vec<String>, cx: &mut ViewContext<Self>) {
|
||||
self.suggestions = suggestions;
|
||||
self.selected_index = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.query.clear();
|
||||
self.suggestions.clear();
|
||||
self.selected_index = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn query(&self) -> &str {
|
||||
&self.query
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Generate Styling
|
||||
|
||||
Create styled variants and theme integration:
|
||||
|
||||
```rust
|
||||
// Component-specific theme
|
||||
pub struct ButtonTheme {
|
||||
pub primary_bg: Hsla,
|
||||
pub primary_fg: Hsla,
|
||||
pub primary_hover: Hsla,
|
||||
pub secondary_bg: Hsla,
|
||||
pub secondary_fg: Hsla,
|
||||
pub secondary_hover: Hsla,
|
||||
pub border_radius: Pixels,
|
||||
pub padding_x: Pixels,
|
||||
pub padding_y: Pixels,
|
||||
}
|
||||
|
||||
impl ButtonTheme {
|
||||
pub fn from_app_theme(theme: &AppTheme) -> Self {
|
||||
Self {
|
||||
primary_bg: theme.colors.primary,
|
||||
primary_fg: theme.colors.primary_foreground,
|
||||
primary_hover: theme.colors.primary_hover,
|
||||
secondary_bg: theme.colors.secondary,
|
||||
secondary_fg: theme.colors.secondary_foreground,
|
||||
secondary_hover: theme.colors.secondary_hover,
|
||||
border_radius: px(6.0),
|
||||
padding_x: px(16.0),
|
||||
padding_y: px(8.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Create Documentation
|
||||
|
||||
Generate comprehensive documentation:
|
||||
|
||||
```rust
|
||||
//! Button Component
|
||||
//!
|
||||
//! A flexible, accessible button component with multiple variants and states.
|
||||
//!
|
||||
//! # Features
|
||||
//!
|
||||
//! - Multiple variants (Primary, Secondary, Destructive, Ghost)
|
||||
//! - Disabled state support
|
||||
//! - Customizable click handlers
|
||||
//! - Full keyboard accessibility
|
||||
//! - Theme integration
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ## Basic Usage
|
||||
//!
|
||||
//! ```rust
|
||||
//! let button = Button::new("Click me")
|
||||
//! .on_click(|cx| {
|
||||
//! println!("Button clicked!");
|
||||
//! });
|
||||
//! ```
|
||||
//!
|
||||
//! ## With Variants
|
||||
//!
|
||||
//! ```rust
|
||||
//! let primary = Button::new("Primary").variant(ButtonVariant::Primary);
|
||||
//! let secondary = Button::new("Secondary").variant(ButtonVariant::Secondary);
|
||||
//! let destructive = Button::new("Delete").variant(ButtonVariant::Destructive);
|
||||
//! ```
|
||||
//!
|
||||
//! ## Disabled State
|
||||
//!
|
||||
//! ```rust
|
||||
//! let button = Button::new("Disabled")
|
||||
//! .disabled(true);
|
||||
//! ```
|
||||
//!
|
||||
//! # Accessibility
|
||||
//!
|
||||
//! - Supports keyboard navigation (Enter/Space to activate)
|
||||
//! - Proper ARIA attributes
|
||||
//! - Focus indicators
|
||||
//! - Disabled state communicated to screen readers
|
||||
```
|
||||
|
||||
### 7. Provide Usage Examples
|
||||
|
||||
Create example usage code:
|
||||
|
||||
```rust
|
||||
// examples/button_example.rs
|
||||
use gpui::*;
|
||||
|
||||
fn main() {
|
||||
App::new("com.example.button-demo", |cx| {
|
||||
cx.open_window(
|
||||
WindowOptions::default(),
|
||||
|cx| cx.new_view(|cx| ButtonDemo::new(cx))
|
||||
)
|
||||
}).run();
|
||||
}
|
||||
|
||||
struct ButtonDemo;
|
||||
|
||||
impl ButtonDemo {
|
||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ButtonDemo {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.p_8()
|
||||
.child(
|
||||
Button::new("Primary Button")
|
||||
.variant(ButtonVariant::Primary)
|
||||
.on_click(|cx| {
|
||||
println!("Primary clicked!");
|
||||
})
|
||||
)
|
||||
.child(
|
||||
Button::new("Secondary Button")
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.on_click(|cx| {
|
||||
println!("Secondary clicked!");
|
||||
})
|
||||
)
|
||||
.child(
|
||||
Button::new("Destructive Button")
|
||||
.variant(ButtonVariant::Destructive)
|
||||
.on_click(|cx| {
|
||||
println!("Destructive clicked!");
|
||||
})
|
||||
)
|
||||
.child(
|
||||
Button::new("Disabled Button")
|
||||
.disabled(true)
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Add Component Tests
|
||||
|
||||
Generate tests for the component:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_button_creation() {
|
||||
App::test(|cx| {
|
||||
let button = Button::new("Test");
|
||||
assert_eq!(button.label, "Test");
|
||||
assert_eq!(button.variant, ButtonVariant::Primary);
|
||||
assert!(!button.disabled);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_button_click() {
|
||||
App::test(|cx| {
|
||||
let clicked = Rc::new(RefCell::new(false));
|
||||
let clicked_clone = clicked.clone();
|
||||
|
||||
let button = Button::new("Test")
|
||||
.on_click(move |_| {
|
||||
*clicked_clone.borrow_mut() = true;
|
||||
});
|
||||
|
||||
// Simulate click
|
||||
if let Some(handler) = button.on_click {
|
||||
handler(cx);
|
||||
}
|
||||
|
||||
assert!(*clicked.borrow());
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Generate Component Module
|
||||
|
||||
Create module file with exports:
|
||||
|
||||
```rust
|
||||
// src/ui/components/button/mod.rs
|
||||
|
||||
mod button;
|
||||
mod theme;
|
||||
|
||||
pub use button::{Button, ButtonVariant};
|
||||
pub use theme::ButtonTheme;
|
||||
```
|
||||
|
||||
### 10. Provide Integration Instructions
|
||||
|
||||
Output integration guide:
|
||||
|
||||
```
|
||||
✓ Created Button component
|
||||
|
||||
Files created:
|
||||
- src/ui/components/button/button.rs
|
||||
- src/ui/components/button/theme.rs
|
||||
- src/ui/components/button/mod.rs
|
||||
- examples/button_example.rs
|
||||
- tests/button_test.rs
|
||||
|
||||
Next steps:
|
||||
|
||||
1. Add to your components module:
|
||||
In src/ui/components/mod.rs:
|
||||
pub mod button;
|
||||
pub use button::Button;
|
||||
|
||||
2. Use in your views:
|
||||
use crate::ui::components::Button;
|
||||
|
||||
Button::new("Click me")
|
||||
.variant(ButtonVariant::Primary)
|
||||
.on_click(|cx| {
|
||||
// Handle click
|
||||
})
|
||||
|
||||
3. Run example:
|
||||
cargo run --example button_example
|
||||
|
||||
4. Run tests:
|
||||
cargo test button
|
||||
|
||||
Documentation: See generated component docs for full API
|
||||
```
|
||||
|
||||
## Component Types
|
||||
|
||||
### Stateless Components
|
||||
- No internal state
|
||||
- Pure rendering based on props
|
||||
- Examples: Button, Icon, Label
|
||||
|
||||
### Stateful Components
|
||||
- Internal state management
|
||||
- User input handling
|
||||
- Examples: Input, SearchBox, Dropdown
|
||||
|
||||
### Container Components
|
||||
- Manage child components
|
||||
- State coordination
|
||||
- Examples: Form, List, Tabs
|
||||
|
||||
### Composite Components
|
||||
- Combine multiple components
|
||||
- Complex functionality
|
||||
- Examples: DataTable, Dialog, Wizard
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Builder pattern for configuration
|
||||
- Theme integration
|
||||
- Accessibility attributes
|
||||
- Comprehensive documentation
|
||||
- Usage examples
|
||||
- Unit tests
|
||||
- Type safety
|
||||
- Error handling
|
||||
|
||||
## Example Usage
|
||||
|
||||
```bash
|
||||
# Generate stateless component
|
||||
/gpui-component Button
|
||||
|
||||
# Generate stateful component
|
||||
/gpui-component SearchInput stateful
|
||||
|
||||
# Generate with custom requirements
|
||||
/gpui-component DataTable stateful --with-examples --with-tests
|
||||
```
|
||||
361
commands/gpui-review.md
Normal file
361
commands/gpui-review.md
Normal file
@@ -0,0 +1,361 @@
|
||||
---
|
||||
name: gpui-review
|
||||
description: Review GPUI code for idiomatic patterns, performance issues, state management correctness, and framework best practices
|
||||
---
|
||||
|
||||
# GPUI Code Review
|
||||
|
||||
Perform comprehensive code review of GPUI applications, identifying issues with patterns, performance, state management, and suggesting improvements.
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$1`: Path (optional) - Specific file or directory to review. If not provided, reviews entire project.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Search for GPUI Code
|
||||
|
||||
- Locate all `.rs` files in the project or specified path
|
||||
- Identify files using GPUI (imports `gpui::*` or specific GPUI types)
|
||||
- Categorize files by type:
|
||||
- View components (impl Render)
|
||||
- Models (application state)
|
||||
- Main entry points
|
||||
- Utility code
|
||||
|
||||
### 2. Analyze Component Patterns
|
||||
|
||||
Review each component for:
|
||||
|
||||
#### Component Structure
|
||||
- [ ] Proper use of View vs Model types
|
||||
- [ ] Clear separation of concerns
|
||||
- [ ] Component has single responsibility
|
||||
- [ ] Props/dependencies passed explicitly
|
||||
- [ ] Proper lifetime management
|
||||
|
||||
#### Anti-patterns
|
||||
- [ ] God components (doing too much)
|
||||
- [ ] Tight coupling between components
|
||||
- [ ] Improper state ownership
|
||||
- [ ] Missing error handling
|
||||
- [ ] Inconsistent naming conventions
|
||||
|
||||
Example issues to flag:
|
||||
|
||||
```rust
|
||||
// BAD: Component doing too much
|
||||
impl Render for GodComponent {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
// Fetching data
|
||||
// Computing business logic
|
||||
// Rendering UI
|
||||
// Handling all events
|
||||
// Managing multiple concerns
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Focused component
|
||||
impl Render for FocusedComponent {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let state = self.model.read(cx);
|
||||
div().child(format!("Count: {}", state.count))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Check State Management
|
||||
|
||||
Review state management for:
|
||||
|
||||
#### Model Usage
|
||||
- [ ] State properly encapsulated in Model types
|
||||
- [ ] Appropriate use of `cx.new_model()`
|
||||
- [ ] State updates use `model.update()`
|
||||
- [ ] No direct state mutation outside updates
|
||||
- [ ] Proper state ownership hierarchy
|
||||
|
||||
#### Subscription Management
|
||||
- [ ] Subscriptions created during initialization, not render
|
||||
- [ ] Subscriptions stored to prevent cleanup
|
||||
- [ ] `cx.observe()` used for model changes
|
||||
- [ ] No subscription leaks
|
||||
- [ ] Proper cleanup in Drop if needed
|
||||
|
||||
Example issues to flag:
|
||||
|
||||
```rust
|
||||
// BAD: Subscription in render (memory leak)
|
||||
impl Render for BadView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
cx.observe(&self.model, |_, _, cx| cx.notify()); // Leak!
|
||||
div()
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Subscription stored
|
||||
struct GoodView {
|
||||
model: Model<Data>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl GoodView {
|
||||
fn new(model: Model<Data>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let _subscription = cx.observe(&model, |_, _, cx| cx.notify());
|
||||
Self { model, _subscription }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Context Usage
|
||||
- [ ] Appropriate context types (WindowContext, ViewContext, ModelContext)
|
||||
- [ ] Global state used sparingly with `cx.global::<T>()`
|
||||
- [ ] Context not stored (lifetime issues)
|
||||
- [ ] Proper use of `cx.notify()` for updates
|
||||
|
||||
### 4. Review Render Performance
|
||||
|
||||
Analyze for performance issues:
|
||||
|
||||
#### Unnecessary Renders
|
||||
- [ ] No expensive computations in render()
|
||||
- [ ] Cached/memoized expensive operations
|
||||
- [ ] Minimal `cx.notify()` calls
|
||||
- [ ] No rendering on every tick/timer
|
||||
- [ ] Proper use of derived state
|
||||
|
||||
#### Element Efficiency
|
||||
- [ ] Minimal element nesting depth
|
||||
- [ ] No repeated identical elements
|
||||
- [ ] Efficient list rendering (consider virtualization)
|
||||
- [ ] Appropriate use of keys for dynamic lists
|
||||
- [ ] No unnecessary cloning in render
|
||||
|
||||
Example issues to flag:
|
||||
|
||||
```rust
|
||||
// BAD: Expensive computation in render
|
||||
impl Render for BadComponent {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let result = expensive_computation(); // Every render!
|
||||
div().child(result)
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Compute on state change
|
||||
struct GoodComponent {
|
||||
cached_result: String,
|
||||
}
|
||||
|
||||
impl GoodComponent {
|
||||
fn update_data(&mut self, data: Data, cx: &mut ViewContext<Self>) {
|
||||
self.cached_result = expensive_computation(&data);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Layout Performance
|
||||
- [ ] Avoid deep nesting
|
||||
- [ ] Use flex layout efficiently
|
||||
- [ ] Minimize layout recalculations
|
||||
- [ ] Appropriate use of fixed vs dynamic sizing
|
||||
- [ ] No layout thrashing
|
||||
|
||||
### 5. Identify Anti-Patterns
|
||||
|
||||
Flag common GPUI anti-patterns:
|
||||
|
||||
#### Memory Issues
|
||||
```rust
|
||||
// BAD: Circular reference
|
||||
struct CircularRef {
|
||||
self_view: Option<View<Self>>, // Circular!
|
||||
}
|
||||
|
||||
// BAD: Unbounded growth
|
||||
struct UnboundedList {
|
||||
items: Vec<Item>, // Grows forever
|
||||
}
|
||||
```
|
||||
|
||||
#### Incorrect Context Usage
|
||||
```rust
|
||||
// BAD: Storing context
|
||||
struct BadComponent {
|
||||
cx: ViewContext<Self>, // Won't compile, lifetime issues
|
||||
}
|
||||
|
||||
// GOOD: Use context in methods
|
||||
impl BadComponent {
|
||||
fn do_something(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// Use cx here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Event Handling Issues
|
||||
```rust
|
||||
// BAD: Not preventing default when needed
|
||||
div()
|
||||
.on_click(|_, cx| {
|
||||
// Action taken but event still propagates
|
||||
})
|
||||
|
||||
// GOOD: Prevent propagation when appropriate
|
||||
div()
|
||||
.on_click(|event, cx| {
|
||||
event.stop_propagation();
|
||||
// Action taken
|
||||
})
|
||||
```
|
||||
|
||||
### 6. Provide Actionable Suggestions
|
||||
|
||||
For each issue found, provide:
|
||||
|
||||
1. **Location**: File and line number
|
||||
2. **Issue**: Clear description of the problem
|
||||
3. **Why**: Explain why it's problematic
|
||||
4. **Fix**: Show concrete code example of how to fix it
|
||||
5. **Priority**: Critical, High, Medium, or Low
|
||||
|
||||
Example output format:
|
||||
|
||||
```
|
||||
📁 src/ui/views/main_view.rs:45
|
||||
|
||||
❌ Issue: Subscription created in render method
|
||||
Severity: Critical (Memory Leak)
|
||||
|
||||
Problem:
|
||||
cx.observe(&self.model, |_, _, cx| cx.notify());
|
||||
|
||||
This creates a new subscription every render, causing a memory leak.
|
||||
Subscriptions are never cleaned up.
|
||||
|
||||
Fix:
|
||||
1. Add subscription field to struct:
|
||||
struct MainView {
|
||||
model: Model<Data>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
2. Create subscription in constructor:
|
||||
fn new(model: Model<Data>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let _subscription = cx.observe(&model, |_, _, cx| cx.notify());
|
||||
Self { model, _subscription }
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Check Framework Best Practices
|
||||
|
||||
Review for GPUI best practices:
|
||||
|
||||
- [ ] Proper use of Element trait
|
||||
- [ ] Appropriate render trait implementations
|
||||
- [ ] Correct action system usage
|
||||
- [ ] Proper theme integration
|
||||
- [ ] Accessibility attributes where appropriate
|
||||
- [ ] Error handling and user feedback
|
||||
- [ ] Type safety leveraged
|
||||
- [ ] Documentation for public APIs
|
||||
|
||||
### 8. Generate Summary Report
|
||||
|
||||
Provide summary including:
|
||||
|
||||
- Total files reviewed
|
||||
- Issues found by severity (Critical, High, Medium, Low)
|
||||
- Common patterns identified
|
||||
- Overall code quality assessment
|
||||
- Top priority fixes
|
||||
- Positive patterns to highlight
|
||||
|
||||
Example summary:
|
||||
|
||||
```
|
||||
GPUI Code Review Summary
|
||||
========================
|
||||
|
||||
Files Reviewed: 15
|
||||
Total Issues: 23
|
||||
|
||||
By Severity:
|
||||
Critical: 2 (Memory leaks, state corruption)
|
||||
High: 5 (Performance issues, anti-patterns)
|
||||
Medium: 10 (Code organization, minor inefficiencies)
|
||||
Low: 6 (Style, documentation)
|
||||
|
||||
Top Priority Fixes:
|
||||
1. Fix subscription leaks in MainView and SidebarView
|
||||
2. Move expensive computations out of render methods
|
||||
3. Implement proper state ownership hierarchy
|
||||
4. Add error handling for async operations
|
||||
5. Reduce component nesting depth in ComplexView
|
||||
|
||||
Positive Patterns:
|
||||
✓ Good separation between UI and business logic
|
||||
✓ Consistent theming throughout
|
||||
✓ Proper use of Model types for state
|
||||
✓ Good component composition in most areas
|
||||
|
||||
Recommendations:
|
||||
- Consider extracting reusable components from large views
|
||||
- Implement virtual scrolling for long lists
|
||||
- Add integration tests for critical user flows
|
||||
- Document component APIs and state flow
|
||||
```
|
||||
|
||||
## Review Categories
|
||||
|
||||
### Architecture
|
||||
- Project structure
|
||||
- Module organization
|
||||
- Dependency management
|
||||
- Separation of concerns
|
||||
|
||||
### State Management
|
||||
- Model usage
|
||||
- Subscription patterns
|
||||
- Context usage
|
||||
- State ownership
|
||||
|
||||
### Performance
|
||||
- Render efficiency
|
||||
- Layout optimization
|
||||
- Memory management
|
||||
- Async operations
|
||||
|
||||
### Code Quality
|
||||
- Idiomatic Rust
|
||||
- Error handling
|
||||
- Type safety
|
||||
- Documentation
|
||||
|
||||
### UI/UX
|
||||
- Component reusability
|
||||
- Consistent styling
|
||||
- Accessibility
|
||||
- User feedback
|
||||
|
||||
## Example Usage
|
||||
|
||||
```bash
|
||||
# Review entire project
|
||||
/gpui-review
|
||||
|
||||
# Review specific file
|
||||
/gpui-review src/ui/views/main_view.rs
|
||||
|
||||
# Review directory
|
||||
/gpui-review src/ui/components/
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Focuses on GPUI-specific patterns and idioms
|
||||
- Considers both correctness and performance
|
||||
- Provides educational feedback with explanations
|
||||
- Prioritizes actionable, concrete suggestions
|
||||
- Highlights both issues and good patterns
|
||||
376
commands/gpui-scaffold.md
Normal file
376
commands/gpui-scaffold.md
Normal file
@@ -0,0 +1,376 @@
|
||||
---
|
||||
name: gpui-scaffold
|
||||
description: Scaffold new GPUI applications with modern structure, Cargo workspace setup, component organization, and best practices
|
||||
---
|
||||
|
||||
# GPUI Project Scaffolding
|
||||
|
||||
Scaffold a new GPUI application with modern project structure, best practices, and example components.
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$1`: Project name (required) - Name for the new GPUI project (hyphen-case recommended)
|
||||
- `$2`: Template type (optional) - Either "app" (default) or "library"
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Validate Project Name
|
||||
|
||||
- Check that project name is provided
|
||||
- Validate naming convention (lowercase, hyphens, no spaces)
|
||||
- Check that directory doesn't already exist
|
||||
- Confirm project creation with user if needed
|
||||
|
||||
### 2. Create Directory Structure
|
||||
|
||||
Create the following structure:
|
||||
|
||||
```
|
||||
project-name/
|
||||
├── Cargo.toml
|
||||
├── .gitignore
|
||||
├── README.md
|
||||
├── src/
|
||||
│ ├── main.rs (for app) or lib.rs (for library)
|
||||
│ ├── app.rs
|
||||
│ ├── ui/
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── views/
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ └── main_view.rs
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ ├── button.rs
|
||||
│ │ │ └── input.rs
|
||||
│ │ └── theme.rs
|
||||
│ ├── models/
|
||||
│ │ ├── mod.rs
|
||||
│ │ └── app_state.rs
|
||||
│ └── utils/
|
||||
│ └── mod.rs
|
||||
├── examples/
|
||||
│ └── basic.rs
|
||||
└── tests/
|
||||
└── integration_test.rs
|
||||
```
|
||||
|
||||
### 3. Generate Cargo.toml
|
||||
|
||||
Create `Cargo.toml` with:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "project-name"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
gpui = { git = "https://github.com/zed-industries/zed" }
|
||||
anyhow = "1.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.5"
|
||||
|
||||
[[bench]]
|
||||
name = "rendering"
|
||||
harness = false
|
||||
```
|
||||
|
||||
### 4. Create Main Entry Point
|
||||
|
||||
For applications (`main.rs`):
|
||||
|
||||
```rust
|
||||
use gpui::*;
|
||||
mod app;
|
||||
mod ui;
|
||||
mod models;
|
||||
mod utils;
|
||||
|
||||
use app::App;
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
App::new("com.example.project-name", |cx| {
|
||||
let app = cx.new_model(|cx| app::AppModel::new(cx));
|
||||
let window = cx.open_window(
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Fixed(Bounds {
|
||||
origin: point(px(100.0), px(100.0)),
|
||||
size: size(px(1200.0), px(800.0)),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| cx.new_view(|cx| ui::views::MainView::new(app.clone(), cx)),
|
||||
);
|
||||
|
||||
window.unwrap()
|
||||
})
|
||||
.run();
|
||||
}
|
||||
```
|
||||
|
||||
For libraries (`lib.rs`):
|
||||
|
||||
```rust
|
||||
pub mod ui;
|
||||
pub mod models;
|
||||
pub mod utils;
|
||||
|
||||
pub use ui::*;
|
||||
pub use models::*;
|
||||
```
|
||||
|
||||
### 5. Create App Model
|
||||
|
||||
Generate `src/app.rs`:
|
||||
|
||||
```rust
|
||||
use gpui::*;
|
||||
use crate::models::AppState;
|
||||
|
||||
pub struct AppModel {
|
||||
state: Model<AppState>,
|
||||
}
|
||||
|
||||
impl AppModel {
|
||||
pub fn new(cx: &mut ModelContext<Self>) -> Self {
|
||||
Self {
|
||||
state: cx.new_model(|_| AppState::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> &Model<AppState> {
|
||||
&self.state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Create Component Structure
|
||||
|
||||
Generate main view (`src/ui/views/main_view.rs`):
|
||||
|
||||
```rust
|
||||
use gpui::*;
|
||||
use crate::app::AppModel;
|
||||
use crate::ui::components::{Button, Input};
|
||||
|
||||
pub struct MainView {
|
||||
app: Model<AppModel>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl MainView {
|
||||
pub fn new(app: Model<AppModel>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let subscription = cx.observe(&app, |_, _, cx| cx.notify());
|
||||
|
||||
Self {
|
||||
app,
|
||||
_subscription: subscription,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MainView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let theme = cx.global::<Theme>();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.bg(theme.background)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.flex_1()
|
||||
.child("Welcome to your GPUI app!")
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Generate reusable components (`src/ui/components/button.rs`, `src/ui/components/input.rs`).
|
||||
|
||||
### 7. Add Example Components
|
||||
|
||||
Create example button component:
|
||||
|
||||
```rust
|
||||
use gpui::*;
|
||||
|
||||
pub struct Button {
|
||||
label: String,
|
||||
on_click: Option<Box<dyn Fn(&mut WindowContext)>>,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(label: impl Into<String>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_click(mut self, handler: impl Fn(&mut WindowContext) + 'static) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Button {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let theme = cx.global::<Theme>();
|
||||
let on_click = self.on_click.take();
|
||||
|
||||
div()
|
||||
.px_4()
|
||||
.py_2()
|
||||
.bg(theme.primary)
|
||||
.text_color(theme.primary_foreground)
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(theme.primary_hover))
|
||||
.when_some(on_click, |this, handler| {
|
||||
this.on_click(move |_, cx| handler(cx))
|
||||
})
|
||||
.child(self.label.clone())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Generate Theme System
|
||||
|
||||
Create `src/ui/theme.rs`:
|
||||
|
||||
```rust
|
||||
use gpui::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Theme {
|
||||
pub background: Hsla,
|
||||
pub foreground: Hsla,
|
||||
pub primary: Hsla,
|
||||
pub primary_foreground: Hsla,
|
||||
pub primary_hover: Hsla,
|
||||
pub border: Hsla,
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
fn default() -> Self {
|
||||
Self::light()
|
||||
}
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn light() -> Self {
|
||||
Self {
|
||||
background: rgb(0xffffff),
|
||||
foreground: rgb(0x000000),
|
||||
primary: rgb(0x2563eb),
|
||||
primary_foreground: rgb(0xffffff),
|
||||
primary_hover: rgb(0x1d4ed8),
|
||||
border: rgb(0xe5e7eb),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dark() -> Self {
|
||||
Self {
|
||||
background: rgb(0x1f2937),
|
||||
foreground: rgb(0xf9fafb),
|
||||
primary: rgb(0x3b82f6),
|
||||
primary_foreground: rgb(0xffffff),
|
||||
primary_hover: rgb(0x2563eb),
|
||||
border: rgb(0x374151),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Generate README
|
||||
|
||||
Create comprehensive README.md with:
|
||||
- Project description
|
||||
- Installation instructions
|
||||
- Usage examples
|
||||
- Development setup
|
||||
- Building and running instructions
|
||||
- Testing guidelines
|
||||
- Contributing information
|
||||
|
||||
### 10. Add .gitignore
|
||||
|
||||
```
|
||||
/target/
|
||||
Cargo.lock
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
```
|
||||
|
||||
### 11. Initialize Git Repository (Optional)
|
||||
|
||||
- Run `git init`
|
||||
- Create initial commit
|
||||
- Ask user if they want to push to remote
|
||||
|
||||
### 12. Provide Next Steps
|
||||
|
||||
Output guidance:
|
||||
```
|
||||
✓ Created GPUI project: project-name
|
||||
|
||||
Next steps:
|
||||
cd project-name
|
||||
cargo build
|
||||
cargo run
|
||||
|
||||
Project structure:
|
||||
- src/main.rs: Application entry point
|
||||
- src/app.rs: Application model
|
||||
- src/ui/: UI components and views
|
||||
- src/models/: Application state models
|
||||
- src/utils/: Utility functions
|
||||
|
||||
To add components:
|
||||
- Create new files in src/ui/components/
|
||||
- Add to src/ui/components/mod.rs
|
||||
- Use in your views
|
||||
|
||||
Documentation:
|
||||
- GPUI: https://github.com/zed-industries/zed/tree/main/crates/gpui
|
||||
- Examples: See examples/ directory
|
||||
```
|
||||
|
||||
## Best Practices Included
|
||||
|
||||
- Modern project structure with clear separation of concerns
|
||||
- Theme system for consistent styling
|
||||
- Reusable component patterns
|
||||
- Example components to get started
|
||||
- Proper subscription management
|
||||
- Type-safe state management
|
||||
- Development tools (examples, tests)
|
||||
|
||||
## Example Usage
|
||||
|
||||
```bash
|
||||
# Scaffold new application
|
||||
/gpui-scaffold my-gpui-app
|
||||
|
||||
# Scaffold library project
|
||||
/gpui-scaffold my-gpui-lib library
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Uses latest GPUI from git (stable API)
|
||||
- Follows Rust 2021 edition conventions
|
||||
- Includes development dependencies for testing
|
||||
- Sets up proper module structure
|
||||
- Includes example code for common patterns
|
||||
491
commands/gpui-test.md
Normal file
491
commands/gpui-test.md
Normal file
@@ -0,0 +1,491 @@
|
||||
---
|
||||
name: gpui-test
|
||||
description: Generate comprehensive tests for GPUI components, views, state management, and user interactions
|
||||
---
|
||||
|
||||
# GPUI Test Generation
|
||||
|
||||
Generate comprehensive tests for GPUI components including unit tests, integration tests, state management tests, and user interaction tests.
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$1`: Component path (required) - Path to the component file to generate tests for
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Analyze Component Structure
|
||||
|
||||
- Read the component file
|
||||
- Identify component type (View, Model, Element)
|
||||
- Extract component struct and fields
|
||||
- Identify render method and UI structure
|
||||
- Find state management patterns
|
||||
- Locate event handlers and actions
|
||||
- Identify dependencies and injected services
|
||||
|
||||
### 2. Generate Unit Tests
|
||||
|
||||
Create unit tests for component logic:
|
||||
|
||||
#### Component Initialization Tests
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::*;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_component_initialization() {
|
||||
App::test(|cx| {
|
||||
let state = cx.new_model(|_| AppState::default());
|
||||
let view = cx.new_view(|cx| MyComponent::new(state.clone(), cx));
|
||||
|
||||
assert!(view.is_some());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_initial_state() {
|
||||
App::test(|cx| {
|
||||
let state = cx.new_model(|_| AppState {
|
||||
count: 0,
|
||||
items: vec![],
|
||||
});
|
||||
let view = cx.new_view(|cx| MyComponent::new(state.clone(), cx));
|
||||
|
||||
view.update(cx, |view, cx| {
|
||||
let state = view.state.read(cx);
|
||||
assert_eq!(state.count, 0);
|
||||
assert_eq!(state.items.len(), 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### State Management Tests
|
||||
|
||||
```rust
|
||||
#[gpui::test]
|
||||
fn test_state_updates() {
|
||||
App::test(|cx| {
|
||||
let state = cx.new_model(|_| AppState { count: 0 });
|
||||
let view = cx.new_view(|cx| Counter::new(state.clone(), cx));
|
||||
|
||||
// Update state
|
||||
state.update(cx, |state, cx| {
|
||||
state.count = 5;
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
// Verify view reflects change
|
||||
view.update(cx, |view, cx| {
|
||||
let state = view.state.read(cx);
|
||||
assert_eq!(state.count, 5);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_subscription_updates() {
|
||||
App::test(|cx| {
|
||||
let state = cx.new_model(|_| AppState { count: 0 });
|
||||
let view = cx.new_view(|cx| Counter::new(state.clone(), cx));
|
||||
|
||||
let initial_render_count = view.render_count();
|
||||
|
||||
// Update should trigger rerender via subscription
|
||||
state.update(cx, |state, cx| {
|
||||
state.count += 1;
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
assert_eq!(view.render_count(), initial_render_count + 1);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Integration Tests
|
||||
|
||||
Generate integration tests for component interactions:
|
||||
|
||||
#### User Interaction Tests
|
||||
|
||||
```rust
|
||||
#[gpui::test]
|
||||
fn test_button_click() {
|
||||
App::test(|cx| {
|
||||
let state = cx.new_model(|_| AppState { count: 0 });
|
||||
let view = cx.new_view(|cx| Counter::new(state.clone(), cx));
|
||||
|
||||
// Simulate button click
|
||||
view.update(cx, |view, cx| {
|
||||
view.handle_increment(cx);
|
||||
});
|
||||
|
||||
// Verify state updated
|
||||
state.update(cx, |state, _| {
|
||||
assert_eq!(state.count, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_input_change() {
|
||||
App::test(|cx| {
|
||||
let state = cx.new_model(|_| FormState::default());
|
||||
let view = cx.new_view(|cx| Form::new(state.clone(), cx));
|
||||
|
||||
// Simulate input change
|
||||
view.update(cx, |view, cx| {
|
||||
view.handle_input_change("test value", cx);
|
||||
});
|
||||
|
||||
// Verify state updated
|
||||
state.update(cx, |state, _| {
|
||||
assert_eq!(state.input_value, "test value");
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Action Handling Tests
|
||||
|
||||
```rust
|
||||
#[gpui::test]
|
||||
fn test_action_dispatch() {
|
||||
App::test(|cx| {
|
||||
let state = cx.new_model(|_| AppState { count: 0 });
|
||||
let view = cx.new_view(|cx| Counter::new(state.clone(), cx));
|
||||
|
||||
// Dispatch action
|
||||
view.update(cx, |view, cx| {
|
||||
cx.dispatch_action(Increment);
|
||||
});
|
||||
|
||||
// Verify action handled
|
||||
state.update(cx, |state, _| {
|
||||
assert_eq!(state.count, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Add Interaction Tests
|
||||
|
||||
Test complex user interactions:
|
||||
|
||||
#### Multi-Step Interactions
|
||||
|
||||
```rust
|
||||
#[gpui::test]
|
||||
fn test_complete_user_flow() {
|
||||
App::test(|cx| {
|
||||
let state = cx.new_model(|_| TodoState::default());
|
||||
let view = cx.new_view(|cx| TodoList::new(state.clone(), cx));
|
||||
|
||||
view.update(cx, |view, cx| {
|
||||
// Add item
|
||||
view.handle_add_todo("Buy milk", cx);
|
||||
|
||||
// Mark complete
|
||||
view.handle_toggle_todo(0, cx);
|
||||
|
||||
// Delete item
|
||||
view.handle_delete_todo(0, cx);
|
||||
});
|
||||
|
||||
state.update(cx, |state, _| {
|
||||
assert_eq!(state.todos.len(), 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Edge Cases
|
||||
|
||||
```rust
|
||||
#[gpui::test]
|
||||
fn test_empty_state() {
|
||||
App::test(|cx| {
|
||||
let state = cx.new_model(|_| AppState::default());
|
||||
let view = cx.new_view(|cx| MyComponent::new(state.clone(), cx));
|
||||
|
||||
// Verify graceful handling of empty state
|
||||
view.update(cx, |view, cx| {
|
||||
let element = view.render(cx);
|
||||
// Assert renders without panic
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_boundary_conditions() {
|
||||
App::test(|cx| {
|
||||
let state = cx.new_model(|_| CounterState { count: i32::MAX });
|
||||
let view = cx.new_view(|cx| Counter::new(state.clone(), cx));
|
||||
|
||||
// Test overflow handling
|
||||
view.update(cx, |view, cx| {
|
||||
view.handle_increment(cx);
|
||||
});
|
||||
|
||||
state.update(cx, |state, _| {
|
||||
// Should handle overflow gracefully
|
||||
assert!(state.count == i32::MAX || state.count == 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Generate Test Utilities
|
||||
|
||||
Create helper functions for testing:
|
||||
|
||||
```rust
|
||||
// Test helpers
|
||||
mod test_utils {
|
||||
use super::*;
|
||||
use gpui::*;
|
||||
|
||||
pub fn create_test_state() -> AppState {
|
||||
AppState {
|
||||
count: 0,
|
||||
items: vec!["item1".to_string(), "item2".to_string()],
|
||||
is_loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_test_view(cx: &mut WindowContext) -> View<MyComponent> {
|
||||
let state = cx.new_model(|_| create_test_state());
|
||||
cx.new_view(|cx| MyComponent::new(state, cx))
|
||||
}
|
||||
|
||||
pub fn assert_state_equals(state: &Model<AppState>, expected_count: i32, cx: &mut AppContext) {
|
||||
state.update(cx, |state, _| {
|
||||
assert_eq!(state.count, expected_count);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Provide Coverage Report
|
||||
|
||||
Generate overview of test coverage:
|
||||
|
||||
```rust
|
||||
// Coverage targets:
|
||||
// - Component initialization: ✓
|
||||
// - State updates: ✓
|
||||
// - User interactions: ✓
|
||||
// - Action handling: ✓
|
||||
// - Edge cases: ✓
|
||||
// - Error handling: ⚠ (needs work)
|
||||
// - Async operations: ⚠ (needs work)
|
||||
```
|
||||
|
||||
### 7. Add Async Tests
|
||||
|
||||
For components with async operations:
|
||||
|
||||
```rust
|
||||
#[gpui::test]
|
||||
async fn test_async_data_loading() {
|
||||
App::test(|cx| async move {
|
||||
let state = cx.new_model(|_| DataState::default());
|
||||
let view = cx.new_view(|cx| DataView::new(state.clone(), cx));
|
||||
|
||||
// Trigger async load
|
||||
view.update(cx, |view, cx| {
|
||||
view.load_data(cx);
|
||||
});
|
||||
|
||||
// Wait for completion
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify data loaded
|
||||
state.update(cx, |state, _| {
|
||||
assert!(state.is_loaded);
|
||||
assert!(!state.data.is_empty());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_async_error_handling() {
|
||||
App::test(|cx| async move {
|
||||
let state = cx.new_model(|_| DataState::default());
|
||||
let view = cx.new_view(|cx| DataView::new(state.clone(), cx));
|
||||
|
||||
// Trigger async operation that will fail
|
||||
view.update(cx, |view, cx| {
|
||||
view.load_data_with_error(cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify error handled
|
||||
state.update(cx, |state, _| {
|
||||
assert!(state.error.is_some());
|
||||
assert!(!state.is_loaded);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Generate Property-Based Tests
|
||||
|
||||
For complex logic, generate property-based tests:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod property_tests {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_counter_never_negative(increments in 0..100u32, decrements in 0..100u32) {
|
||||
App::test(|cx| {
|
||||
let state = cx.new_model(|_| CounterState { count: 0 });
|
||||
|
||||
state.update(cx, |state, _| {
|
||||
for _ in 0..increments {
|
||||
state.count += 1;
|
||||
}
|
||||
for _ in 0..decrements {
|
||||
state.count = state.count.saturating_sub(1);
|
||||
}
|
||||
});
|
||||
|
||||
state.update(cx, |state, _| {
|
||||
prop_assert!(state.count >= 0);
|
||||
Ok(())
|
||||
}).unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Add Benchmark Tests
|
||||
|
||||
Create performance benchmarks:
|
||||
|
||||
```rust
|
||||
// benches/component_bench.rs
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
fn render_benchmark(c: &mut Criterion) {
|
||||
c.bench_function("component render", |b| {
|
||||
App::test(|cx| {
|
||||
let state = cx.new_model(|_| create_large_state());
|
||||
let view = cx.new_view(|cx| MyComponent::new(state, cx));
|
||||
|
||||
b.iter(|| {
|
||||
view.update(cx, |view, cx| {
|
||||
black_box(view.render(cx));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, render_benchmark);
|
||||
criterion_main!(benches);
|
||||
```
|
||||
|
||||
### 10. Generate Test Documentation
|
||||
|
||||
Create documentation for tests:
|
||||
|
||||
```rust
|
||||
//! Component Tests
|
||||
//!
|
||||
//! This module contains comprehensive tests for MyComponent including:
|
||||
//!
|
||||
//! - Unit tests for component logic and state management
|
||||
//! - Integration tests for user interactions
|
||||
//! - Async tests for data loading
|
||||
//! - Property-based tests for invariants
|
||||
//! - Performance benchmarks
|
||||
//!
|
||||
//! ## Running Tests
|
||||
//!
|
||||
//! ```bash
|
||||
//! # Run all tests
|
||||
//! cargo test
|
||||
//!
|
||||
//! # Run specific test
|
||||
//! cargo test test_state_updates
|
||||
//!
|
||||
//! # Run with output
|
||||
//! cargo test -- --nocapture
|
||||
//!
|
||||
//! # Run benchmarks
|
||||
//! cargo bench
|
||||
//! ```
|
||||
```
|
||||
|
||||
## Test Categories
|
||||
|
||||
### Unit Tests
|
||||
- Component initialization
|
||||
- State management
|
||||
- Helper functions
|
||||
- Pure logic
|
||||
|
||||
### Integration Tests
|
||||
- User interactions
|
||||
- Component composition
|
||||
- Event propagation
|
||||
- Action handling
|
||||
|
||||
### UI Tests
|
||||
- Render output
|
||||
- Layout calculations
|
||||
- Style application
|
||||
- Theme integration
|
||||
|
||||
### Performance Tests
|
||||
- Render benchmarks
|
||||
- State update performance
|
||||
- Memory usage
|
||||
- Subscription efficiency
|
||||
|
||||
## Example Usage
|
||||
|
||||
```bash
|
||||
# Generate tests for specific component
|
||||
/gpui-test src/ui/views/counter.rs
|
||||
|
||||
# Generate tests for all components in directory
|
||||
/gpui-test src/ui/components/
|
||||
|
||||
# Generate tests with benchmarks
|
||||
/gpui-test src/ui/views/data_view.rs --with-benchmarks
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Test behavior, not implementation
|
||||
- Use descriptive test names
|
||||
- Test edge cases and error conditions
|
||||
- Keep tests focused and independent
|
||||
- Use test utilities for common setup
|
||||
- Document complex test scenarios
|
||||
- Maintain high coverage (>80%)
|
||||
- Run tests in CI/CD pipeline
|
||||
|
||||
## Output Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── component_name_test.rs
|
||||
│ ├── Unit tests
|
||||
│ ├── Integration tests
|
||||
│ └── Test utilities
|
||||
└── benches/
|
||||
└── component_name_bench.rs
|
||||
```
|
||||
Reference in New Issue
Block a user