Improve storybook story selection (#3653)

Marshall Bowers created

This PR builds on top of #3652 by adding a selection prompt to the
storybook to allow you to choose from the available list of stories if
you don't provide one explicitly:

<img width="1387" alt="Screenshot 2023-12-14 at 12 00 26 PM"
src="https://github.com/zed-industries/zed/assets/1486634/640d62a3-1340-45f1-9746-69b513faff62">

This way we don't have to keep generating the `script/storybook` script
whenever stories are added/removed.

#### Usage (through `cargo`):

```sh
# Select from the available stories
cargo run -p storybook2

# Run a specific story
cargo run -p storybook2 -- components/list_item
```

#### Usage (through `script/storybook`):

```sh
# Select from the available stories
./script/storybook

# Run a specific story
./script/storybook list_item
```

Release Notes:

- N/A

Change summary

Cargo.lock                                          | 100 +++++++++++---
crates/storybook2/Cargo.toml                        |  10 -
crates/storybook2/src/bin/print_storybook_script.rs |  74 -----------
crates/storybook2/src/lib.rs                        |   4 
crates/storybook2/src/storybook2.rs                 |  25 +++
script/storybook                                    |  61 --------
6 files changed, 106 insertions(+), 168 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -200,7 +200,7 @@ dependencies = [
  "toml 0.7.8",
  "unicode-width",
  "vte",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -293,7 +293,7 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
 dependencies = [
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -303,7 +303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
 dependencies = [
  "anstyle",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -580,7 +580,7 @@ dependencies = [
  "futures-lite",
  "rustix 0.37.23",
  "signal-hook",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -2085,6 +2085,19 @@ dependencies = [
  "crossbeam-utils",
 ]
 
+[[package]]
+name = "console"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8"
+dependencies = [
+ "encode_unicode",
+ "lazy_static",
+ "libc",
+ "unicode-width",
+ "windows-sys 0.45.0",
+]
+
 [[package]]
 name = "const-cstr"
 version = "0.3.0"
@@ -2572,7 +2585,7 @@ dependencies = [
  "openssl-sys",
  "pkg-config",
  "vcpkg",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -2781,6 +2794,20 @@ dependencies = [
  "workspace2",
 ]
 
+[[package]]
+name = "dialoguer"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
+dependencies = [
+ "console",
+ "fuzzy-matcher",
+ "shell-words",
+ "tempfile",
+ "thiserror",
+ "zeroize",
+]
+
 [[package]]
 name = "diff"
 version = "0.1.13"
@@ -3026,6 +3053,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "encode_unicode"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
+
 [[package]]
 name = "encoding_rs"
 version = "0.8.33"
@@ -3104,7 +3137,7 @@ checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
 dependencies = [
  "errno-dragonfly",
  "libc",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -3135,7 +3168,7 @@ checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
 dependencies = [
  "cfg-if 1.0.0",
  "home",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -3343,7 +3376,7 @@ dependencies = [
  "cfg-if 1.0.0",
  "libc",
  "redox_syscall 0.3.5",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -3722,6 +3755,15 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "fuzzy-matcher"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
+dependencies = [
+ "thread_local",
+]
+
 [[package]]
 name = "fuzzy2"
 version = "0.1.0"
@@ -4243,7 +4285,7 @@ version = "0.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
 dependencies = [
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -4520,7 +4562,7 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
 dependencies = [
  "hermit-abi 0.3.3",
  "libc",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -4577,7 +4619,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
 dependencies = [
  "hermit-abi 0.3.3",
  "rustix 0.38.14",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -5000,7 +5042,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb"
 dependencies = [
  "cfg-if 1.0.0",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -5493,7 +5535,7 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
 dependencies = [
  "libc",
  "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -6674,7 +6716,7 @@ dependencies = [
  "libc",
  "log",
  "pin-project-lite 0.2.13",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -7980,7 +8022,7 @@ dependencies = [
  "io-lifetimes 1.0.11",
  "libc",
  "linux-raw-sys 0.3.8",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -7993,7 +8035,7 @@ dependencies = [
  "errno 0.3.3",
  "libc",
  "linux-raw-sys 0.4.7",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -8106,7 +8148,7 @@ version = "0.1.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
 dependencies = [
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -8706,6 +8748,12 @@ dependencies = [
  "lazy_static",
 ]
 
+[[package]]
+name = "shell-words"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
+
 [[package]]
 name = "shellexpand"
 version = "2.1.2"
@@ -8905,7 +8953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
 dependencies = [
  "libc",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -9221,6 +9269,7 @@ dependencies = [
  "backtrace-on-stack-overflow",
  "chrono",
  "clap 4.4.4",
+ "dialoguer",
  "editor2",
  "fuzzy2",
  "gpui2",
@@ -9521,7 +9570,7 @@ dependencies = [
  "fastrand 2.0.0",
  "redox_syscall 0.3.5",
  "rustix 0.38.14",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -9960,7 +10009,7 @@ dependencies = [
  "signal-hook-registry",
  "socket2 0.5.4",
  "tokio-macros",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -11590,6 +11639,15 @@ dependencies = [
  "windows-targets 0.48.5",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.48.0"
@@ -11729,7 +11787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
 dependencies = [
  "cfg-if 1.0.0",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]

crates/storybook2/Cargo.toml 🔗

@@ -3,23 +3,19 @@ name = "storybook2"
 version = "0.1.0"
 edition = "2021"
 publish = false
-default-run = "storybook"
 
 [[bin]]
 name = "storybook"
-path = "src/bin/storybook2.rs"
-
-[[bin]]
-name = "print_storybook_script"
-path = "src/bin/print_storybook_script.rs"
+path = "src/storybook2.rs"
 
 [dependencies]
 anyhow.workspace = true
 # TODO: Remove after diagnosing stack overflow.
 backtrace-on-stack-overflow = "0.3.0"
+chrono = "0.4"
 clap = { version = "4.4", features = ["derive", "string"] }
+dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
 editor = { package = "editor2", path = "../editor2" }
-chrono = "0.4"
 fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
 gpui = { package = "gpui2", path = "../gpui2" }
 itertools = "0.11.0"

crates/storybook2/src/bin/print_storybook_script.rs 🔗

@@ -1,74 +0,0 @@
-use std::fs::File;
-use std::io::Write;
-
-use strum::IntoEnumIterator;
-
-use storybook2::story_selector::ComponentStory;
-
-// TOOD: Ideally we actually create a more full featured CLI,
-// but for the moment I just wanted a easier way to run the stories
-
-fn main() -> std::io::Result<()> {
-    let path = std::env::current_dir()?;
-    let out_file = path.join("script").join("storybook");
-
-    // the script output file
-    let mut file = File::create(out_file)?;
-
-    // generate the list of components, in `snake_case`
-    let components = ComponentStory::iter()
-        .map(|c| c.to_string()) // Converts enum to string in `snake_case`
-        .collect::<Vec<_>>();
-
-    // write the bash script
-    writeln!(file, "#!/bin/bash")?;
-    writeln!(file, "")?;
-    writeln!(file, "options=(")?;
-    for component in &components {
-        writeln!(file, "  \"{}\"", component)?;
-    }
-    writeln!(file, ")")?;
-    writeln!(file, "")?;
-
-    // Check if an argument is provided and if it matches a valid option
-    writeln!(file, "run_story() {{")?;
-    writeln!(file, "  echo \"Running story: $1\"")?;
-    writeln!(file, "  cargo run -p storybook2 -- \"$1\"")?;
-    writeln!(file, "}}")?;
-    writeln!(file, "")?;
-
-    writeln!(file, "if [ \"$#\" -gt 0 ]; then")?;
-    writeln!(file, "  story_arg=\"$1\"")?;
-    writeln!(file, "  # Add prefix 'components/' if not present")?;
-    writeln!(file, "  if [[ $story_arg != components/* ]]; then")?;
-    writeln!(file, "    story_arg=\"components/$story_arg\"")?;
-    writeln!(file, "  fi")?;
-    writeln!(file, "  # Check if the provided story is a valid option")?;
-    writeln!(file, "  for opt in \"${{options[@]}}\"; do")?;
-    writeln!(
-        file,
-        "    if [[ \"components/$opt\" == \"$story_arg\" ]]; then"
-    )?;
-    writeln!(file, "      run_story \"$story_arg\"")?;
-    writeln!(file, "      exit")?;
-    writeln!(file, "    fi")?;
-    writeln!(file, "  done")?;
-    writeln!(file, "  echo \"Invalid story name: $1\"")?;
-    writeln!(file, "  exit 1")?;
-    writeln!(file, "fi")?;
-    writeln!(file, "")?;
-
-    // Existing selection prompt
-    writeln!(file, "prompt=\"Please select a story:\"")?;
-    writeln!(file, "PS3=\"$prompt \"")?;
-    writeln!(file, "select story in \"${{options[@]}}\"; do")?;
-    writeln!(file, "  if [[ -n $story ]]; then")?;
-    writeln!(file, "    run_story \"components/$story\"")?;
-    writeln!(file, "    break")?;
-    writeln!(file, "  else")?;
-    writeln!(file, "    echo \"Invalid option\"")?;
-    writeln!(file, "  fi")?;
-    writeln!(file, "done")?;
-
-    Ok(())
-}

crates/storybook2/src/bin/storybook2.rs → crates/storybook2/src/storybook2.rs 🔗

@@ -1,6 +1,11 @@
+mod assets;
+mod stories;
+mod story_selector;
+
 use std::sync::Arc;
 
 use clap::Parser;
+use dialoguer::FuzzySelect;
 use gpui::{
     div, px, size, AnyView, AppContext, Bounds, Div, Render, ViewContext, VisualContext,
     WindowBounds, WindowOptions,
@@ -8,12 +13,12 @@ use gpui::{
 use log::LevelFilter;
 use settings2::{default_settings, Settings, SettingsStore};
 use simplelog::SimpleLogger;
+use strum::IntoEnumIterator;
 use theme2::{ThemeRegistry, ThemeSettings};
 use ui::prelude::*;
 
-use storybook2::assets::Assets;
-pub use storybook2::story_selector::*;
-// pub use crate::story_selector::{ComponentStory, StorySelector};
+use crate::assets::Assets;
+use crate::story_selector::{ComponentStory, StorySelector};
 
 // gpui::actions! {
 //     storybook,
@@ -40,7 +45,17 @@ fn main() {
 
     let args = Args::parse();
 
-    let story_selector = args.story.clone();
+    let story_selector = args.story.clone().unwrap_or_else(|| {
+        let stories = ComponentStory::iter().collect::<Vec<_>>();
+
+        let selection = FuzzySelect::new()
+            .with_prompt("Choose a story to run:")
+            .items(&stories)
+            .interact()
+            .unwrap();
+
+        StorySelector::Component(stories[selection])
+    });
     let theme_name = args.theme.unwrap_or("One Dark".to_string());
 
     let asset_source = Arc::new(Assets);
@@ -55,7 +70,7 @@ fn main() {
 
         theme2::init(theme2::LoadThemes::All, cx);
 
-        let selector = story_selector.unwrap_or(StorySelector::KitchenSink);
+        let selector = story_selector;
 
         let theme_registry = cx.global::<ThemeRegistry>();
         let mut theme_settings = ThemeSettings::get_global(cx).clone();

script/storybook 🔗

@@ -1,60 +1,7 @@
 #!/bin/bash
 
-options=(
-  "auto_height_editor"
-  "avatar"
-  "button"
-  "checkbox"
-  "context_menu"
-  "cursor"
-  "disclosure"
-  "focus"
-  "icon"
-  "icon_button"
-  "keybinding"
-  "label"
-  "list"
-  "list_header"
-  "list_item"
-  "overflow_scroll"
-  "scroll"
-  "tab"
-  "tab_bar"
-  "text"
-  "viewport_units"
-  "z_index"
-  "picker"
-)
-
-run_story() {
-  echo "Running story: $1"
-  cargo run -p storybook2 -- "$1"
-}
-
-if [ "$#" -gt 0 ]; then
-  story_arg="$1"
-  # Add prefix 'components/' if not present
-  if [[ $story_arg != components/* ]]; then
-    story_arg="components/$story_arg"
-  fi
-  # Check if the provided story is a valid option
-  for opt in "${options[@]}"; do
-    if [[ "components/$opt" == "$story_arg" ]]; then
-      run_story "$story_arg"
-      exit
-    fi
-  done
-  echo "Invalid story name: $1"
-  exit 1
+if [ -z "$1" ]; then
+    cargo run -p storybook2
+else
+    cargo run -p storybook2 -- "components/$1"
 fi
-
-prompt="Please select a story:"
-PS3="$prompt "
-select story in "${options[@]}"; do
-  if [[ -n $story ]]; then
-    run_story "components/$story"
-    break
-  else
-    echo "Invalid option"
-  fi
-done