Display available stories in storybook CLI (#3021)

Marshall Bowers created

This PR updates the storybook CLI to support displaying all of the
available stories.

The `--help` flag will now show a list of all the available stories:

<img width="1435" alt="Screenshot 2023-09-22 at 6 11 00 PM"
src="https://github.com/zed-industries/zed/assets/1486634/284e1a24-46ec-462e-9709-0f9b6e94931f">

Inputting an invalid story name will also show the list of available
stories:

<img width="1435" alt="Screenshot 2023-09-22 at 6 10 43 PM"
src="https://github.com/zed-industries/zed/assets/1486634/1ce3ae3f-ab03-4976-a06a-5a2b5f61eae3">

Release Notes:

- N/A

Change summary

Cargo.lock                             |  2 
crates/storybook/Cargo.toml            |  2 
crates/storybook/src/story_selector.rs | 76 ++++++++++++++++++++++++++++
crates/storybook/src/storybook.rs      | 55 ++------------------
4 files changed, 84 insertions(+), 51 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7374,7 +7374,7 @@ name = "storybook"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "clap 3.2.25",
+ "clap 4.4.4",
  "gpui2",
  "log",
  "rust-embed",

crates/storybook/Cargo.toml 🔗

@@ -10,7 +10,7 @@ path = "src/storybook.rs"
 
 [dependencies]
 anyhow.workspace = true
-clap = { version = "3.1", features = ["derive"] }
+clap = { version = "4.4", features = ["derive", "string"] }
 gpui2 = { path = "../gpui2" }
 log.workspace = true
 rust-embed.workspace = true

crates/storybook/src/story_selector.rs 🔗

@@ -0,0 +1,76 @@
+use std::{str::FromStr, sync::OnceLock};
+
+use anyhow::{anyhow, Context};
+use clap::builder::PossibleValue;
+use clap::ValueEnum;
+use strum::{EnumIter, EnumString, IntoEnumIterator};
+
+#[derive(Debug, Clone, Copy, strum::Display, EnumString, EnumIter)]
+#[strum(serialize_all = "snake_case")]
+pub enum ElementStory {
+    Avatar,
+}
+
+#[derive(Debug, Clone, Copy, strum::Display, EnumString, EnumIter)]
+#[strum(serialize_all = "snake_case")]
+pub enum ComponentStory {
+    Breadcrumb,
+    Facepile,
+    Toolbar,
+    TrafficLights,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum StorySelector {
+    Element(ElementStory),
+    Component(ComponentStory),
+}
+
+impl FromStr for StorySelector {
+    type Err = anyhow::Error;
+
+    fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
+        let story = raw_story_name.to_ascii_lowercase();
+
+        if let Some((_, story)) = story.split_once("elements/") {
+            let element_story = ElementStory::from_str(story)
+                .with_context(|| format!("story not found for element '{story}'"))?;
+
+            return Ok(Self::Element(element_story));
+        }
+
+        if let Some((_, story)) = story.split_once("components/") {
+            let component_story = ComponentStory::from_str(story)
+                .with_context(|| format!("story not found for component '{story}'"))?;
+
+            return Ok(Self::Component(component_story));
+        }
+
+        Err(anyhow!("story not found for '{raw_story_name}'"))
+    }
+}
+
+/// The list of all stories available in the storybook.
+static ALL_STORIES: OnceLock<Vec<StorySelector>> = OnceLock::new();
+
+impl ValueEnum for StorySelector {
+    fn value_variants<'a>() -> &'a [Self] {
+        let stories = ALL_STORIES.get_or_init(|| {
+            let element_stories = ElementStory::iter().map(Self::Element);
+            let component_stories = ComponentStory::iter().map(Self::Component);
+
+            element_stories.chain(component_stories).collect::<Vec<_>>()
+        });
+
+        stories
+    }
+
+    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
+        let value = match self {
+            Self::Element(story) => format!("elements/{story}"),
+            Self::Component(story) => format!("components/{story}"),
+        };
+
+        Some(PossibleValue::new(value))
+    }
+}

crates/storybook/src/storybook.rs 🔗

@@ -3,10 +3,9 @@
 mod collab_panel;
 mod stories;
 mod story;
+mod story_selector;
 mod workspace;
 
-use std::str::FromStr;
-
 use ::theme as legacy_theme;
 use clap::Parser;
 use gpui2::{serde_json, vec2f, view, Element, IntoElement, RectF, ViewContext, WindowBounds};
@@ -19,61 +18,19 @@ use stories::components::facepile::FacepileStory;
 use stories::components::toolbar::ToolbarStory;
 use stories::components::traffic_lights::TrafficLightsStory;
 use stories::elements::avatar::AvatarStory;
-use strum::EnumString;
 use ui::{ElementExt, Theme};
 
+use crate::story_selector::{ComponentStory, ElementStory, StorySelector};
+
 gpui2::actions! {
     storybook,
     [ToggleInspector]
 }
 
-#[derive(Debug, Clone, Copy)]
-enum StorySelector {
-    Element(ElementStory),
-    Component(ComponentStory),
-}
-
-impl FromStr for StorySelector {
-    type Err = anyhow::Error;
-
-    fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
-        let story = raw_story_name.to_ascii_lowercase();
-
-        if let Some((_, story)) = story.split_once("elements/") {
-            let element_story = ElementStory::from_str(story)
-                .with_context(|| format!("story not found for element '{story}'"))?;
-
-            return Ok(Self::Element(element_story));
-        }
-
-        if let Some((_, story)) = story.split_once("components/") {
-            let component_story = ComponentStory::from_str(story)
-                .with_context(|| format!("story not found for component '{story}'"))?;
-
-            return Ok(Self::Component(component_story));
-        }
-
-        Err(anyhow!("story not found for '{raw_story_name}'"))
-    }
-}
-
-#[derive(Debug, Clone, Copy, EnumString)]
-#[strum(serialize_all = "snake_case")]
-enum ElementStory {
-    Avatar,
-}
-
-#[derive(Debug, Clone, Copy, EnumString)]
-#[strum(serialize_all = "snake_case")]
-enum ComponentStory {
-    Breadcrumb,
-    Facepile,
-    Toolbar,
-    TrafficLights,
-}
-
 #[derive(Parser)]
+#[command(author, version, about, long_about = None)]
 struct Args {
+    #[arg(value_enum)]
     story: Option<StorySelector>,
 }
 
@@ -146,7 +103,7 @@ fn current_theme<V: 'static>(cx: &mut ViewContext<V>) -> Theme {
         .clone()
 }
 
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Result};
 use gpui2::AssetSource;
 use rust_embed::RustEmbed;
 use workspace::WorkspaceElement;