Remove `zed` dependency from `docs_preprocessor` (#45130)

Ben Kunkle and Zed Zippy created

Closes #ISSUE

Uses the existing `--dump-all-actions` arg on the Zed binary to generate
an asset of all of our actions so that the `docs_preprocessor` can
injest it, rather than depending on the Zed crate itself to collect all
action names

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>

Change summary

.github/workflows/run_tests.yml                |  3 
.gitignore                                     |  1 
Cargo.lock                                     |  3 
crates/docs_preprocessor/Cargo.toml            |  5 
crates/docs_preprocessor/src/main.rs           | 88 +++++++++++--------
crates/title_bar/src/application_menu.rs       | 20 ++--
crates/zed/Cargo.toml                          |  4 
crates/zed/src/main.rs                         | 13 +-
crates/zed/src/zed-main.rs                     |  8 -
crates/zed/src/zed.rs                          |  1 
script/generate-action-metadata                | 10 ++
tooling/xtask/src/tasks/workflows/run_tests.rs |  1 
12 files changed, 83 insertions(+), 74 deletions(-)

Detailed changes

.github/workflows/run_tests.yml 🔗

@@ -353,6 +353,9 @@ jobs:
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
       shell: bash -euxo pipefail {0}
+    - name: ./script/generate-action-metadata
+      run: ./script/generate-action-metadata
+      shell: bash -euxo pipefail {0}
     - name: run_tests::check_docs::install_mdbook
       uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08
       with:

.gitignore 🔗

@@ -36,6 +36,7 @@
 DerivedData/
 Packages
 xcuserdata/
+crates/docs_preprocessor/actions.json
 
 # Don't commit any secrets to the repo.
 .env

Cargo.lock 🔗

@@ -5021,8 +5021,6 @@ name = "docs_preprocessor"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "command_palette",
- "gpui",
  "mdbook",
  "regex",
  "serde",
@@ -5031,7 +5029,6 @@ dependencies = [
  "task",
  "theme",
  "util",
- "zed",
  "zlog",
 ]
 

crates/docs_preprocessor/Cargo.toml 🔗

@@ -7,8 +7,6 @@ license = "GPL-3.0-or-later"
 
 [dependencies]
 anyhow.workspace = true
-command_palette.workspace = true
-gpui.workspace = true
 # We are specifically pinning this version of mdbook, as later versions introduce issues with double-nested subdirectories.
 # Ask @maxdeviant about this before bumping.
 mdbook = "= 0.4.40"
@@ -17,7 +15,6 @@ serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
 util.workspace = true
-zed.workspace = true
 zlog.workspace = true
 task.workspace = true
 theme.workspace = true
@@ -27,4 +24,4 @@ workspace = true
 
 [[bin]]
 name = "docs_preprocessor"
-path = "src/main.rs"
+path = "src/main.rs"

crates/docs_preprocessor/src/main.rs 🔗

@@ -22,16 +22,13 @@ static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
     load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
 });
 
-static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
+static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(load_all_actions);
 
 const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
 
 fn main() -> Result<()> {
     zlog::init();
     zlog::init_output_stderr();
-    // call a zed:: function so everything in `zed` crate is linked and
-    // all actions in the actual app are registered
-    zed::stdout_is_a_pty();
     let args = std::env::args().skip(1).collect::<Vec<_>>();
 
     match args.get(0).map(String::as_str) {
@@ -72,8 +69,8 @@ enum PreprocessorError {
 impl PreprocessorError {
     fn new_for_not_found_action(action_name: String) -> Self {
         for action in &*ALL_ACTIONS {
-            for alias in action.deprecated_aliases {
-                if alias == &action_name {
+            for alias in &action.deprecated_aliases {
+                if alias == action_name.as_str() {
                     return PreprocessorError::DeprecatedActionUsed {
                         used: action_name,
                         should_be: action.name.to_string(),
@@ -214,7 +211,7 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Prepr
         chapter.content = regex
             .replace_all(&chapter.content, |caps: &regex::Captures| {
                 let action = caps[1].trim();
-                if find_action_by_name(action).is_none() {
+                if is_missing_action(action) {
                     errors.insert(PreprocessorError::new_for_not_found_action(
                         action.to_string(),
                     ));
@@ -244,10 +241,12 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
             .replace_all(&chapter.content, |caps: &regex::Captures| {
                 let name = caps[1].trim();
                 let Some(action) = find_action_by_name(name) else {
-                    errors.insert(PreprocessorError::new_for_not_found_action(
-                        name.to_string(),
-                    ));
-                    return String::new();
+                    if actions_available() {
+                        errors.insert(PreprocessorError::new_for_not_found_action(
+                            name.to_string(),
+                        ));
+                    }
+                    return format!("<code class=\"hljs\">{}</code>", name);
                 };
                 format!("<code class=\"hljs\">{}</code>", &action.human_name)
             })
@@ -257,11 +256,19 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
 
 fn find_action_by_name(name: &str) -> Option<&ActionDef> {
     ALL_ACTIONS
-        .binary_search_by(|action| action.name.cmp(name))
+        .binary_search_by(|action| action.name.as_str().cmp(name))
         .ok()
         .map(|index| &ALL_ACTIONS[index])
 }
 
+fn actions_available() -> bool {
+    !ALL_ACTIONS.is_empty()
+}
+
+fn is_missing_action(name: &str) -> bool {
+    actions_available() && find_action_by_name(name).is_none()
+}
+
 fn find_binding(os: &str, action: &str) -> Option<String> {
     let keymap = match os {
         "macos" => &KEYMAP_MACOS,
@@ -384,18 +391,13 @@ fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<Pre
                 let keymap = settings::KeymapFile::parse(&snippet_json_fixed)
                     .context("Failed to parse keymap JSON")?;
                 for section in keymap.sections() {
-                    for (keystrokes, action) in section.bindings() {
-                        keystrokes
-                            .split_whitespace()
-                            .map(|source| gpui::Keystroke::parse(source))
-                            .collect::<std::result::Result<Vec<_>, _>>()
-                            .context("Failed to parse keystroke")?;
+                    for (_keystrokes, action) in section.bindings() {
                         if let Some((action_name, _)) = settings::KeymapFile::parse_action(action)
                             .map_err(|err| anyhow::format_err!(err))
                             .context("Failed to parse action")?
                         {
                             anyhow::ensure!(
-                                find_action_by_name(action_name).is_some(),
+                                !is_missing_action(action_name),
                                 "Action not found: {}",
                                 action_name
                             );
@@ -491,27 +493,35 @@ where
     });
 }
 
-#[derive(Debug, serde::Serialize)]
+#[derive(Debug, serde::Serialize, serde::Deserialize)]
 struct ActionDef {
-    name: &'static str,
+    name: String,
     human_name: String,
-    deprecated_aliases: &'static [&'static str],
-    docs: Option<&'static str>,
+    deprecated_aliases: Vec<String>,
+    #[serde(rename = "documentation")]
+    docs: Option<String>,
 }
 
-fn dump_all_gpui_actions() -> Vec<ActionDef> {
-    let mut actions = gpui::generate_list_of_all_registered_actions()
-        .map(|action| ActionDef {
-            name: action.name,
-            human_name: command_palette::humanize_action_name(action.name),
-            deprecated_aliases: action.deprecated_aliases,
-            docs: action.documentation,
-        })
-        .collect::<Vec<ActionDef>>();
-
-    actions.sort_by_key(|a| a.name);
-
-    actions
+fn load_all_actions() -> Vec<ActionDef> {
+    let asset_path = concat!(env!("CARGO_MANIFEST_DIR"), "/actions.json");
+    match std::fs::read_to_string(asset_path) {
+        Ok(content) => {
+            let mut actions: Vec<ActionDef> =
+                serde_json::from_str(&content).expect("Failed to parse actions.json");
+            actions.sort_by(|a, b| a.name.cmp(&b.name));
+            actions
+        }
+        Err(err) => {
+            if std::env::var("CI").is_ok() {
+                panic!("actions.json not found at {}: {}", asset_path, err);
+            }
+            eprintln!(
+                "Warning: actions.json not found, action validation will be skipped: {}",
+                err
+            );
+            Vec::new()
+        }
+    }
 }
 
 fn handle_postprocessing() -> Result<()> {
@@ -647,7 +657,7 @@ fn generate_big_table_of_actions() -> String {
     let mut output = String::new();
 
     let mut actions_sorted = actions.iter().collect::<Vec<_>>();
-    actions_sorted.sort_by_key(|a| a.name);
+    actions_sorted.sort_by_key(|a| a.name.as_str());
 
     // Start the definition list with custom styling for better spacing
     output.push_str("<dl style=\"line-height: 1.8;\">\n");
@@ -664,7 +674,7 @@ fn generate_big_table_of_actions() -> String {
         output.push_str("<dd style=\"margin-left: 2em; margin-bottom: 1em;\">\n");
 
         // Add the description, escaping HTML if needed
-        if let Some(description) = action.docs {
+        if let Some(description) = action.docs.as_ref() {
             output.push_str(
                 &description
                     .replace("&", "&amp;")
@@ -674,7 +684,7 @@ fn generate_big_table_of_actions() -> String {
             output.push_str("<br>\n");
         }
         output.push_str("Keymap Name: <code>");
-        output.push_str(action.name);
+        output.push_str(&action.name);
         output.push_str("</code><br>\n");
         if !action.deprecated_aliases.is_empty() {
             output.push_str("Deprecated Alias(es): ");

crates/title_bar/src/application_menu.rs 🔗

@@ -1,12 +1,7 @@
-use gpui::{Entity, OwnedMenu, OwnedMenuItem};
+use gpui::{Action, Entity, OwnedMenu, OwnedMenuItem, actions};
 use settings::Settings;
 
-#[cfg(not(target_os = "macos"))]
-use gpui::{Action, actions};
-
-#[cfg(not(target_os = "macos"))]
 use schemars::JsonSchema;
-#[cfg(not(target_os = "macos"))]
 use serde::Deserialize;
 
 use smallvec::SmallVec;
@@ -14,18 +9,23 @@ use ui::{ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
 
 use crate::title_bar_settings::TitleBarSettings;
 
-#[cfg(not(target_os = "macos"))]
 actions!(
     app_menu,
     [
-        /// Navigates to the menu item on the right.
+        /// Activates the menu on the right in the client-side application menu.
+        ///
+        /// Does not apply to platform menu bars (e.g. on macOS).
         ActivateMenuRight,
-        /// Navigates to the menu item on the left.
+        /// Activates the menu on the left in the client-side application menu.
+        ///
+        /// Does not apply to platform menu bars (e.g. on macOS).
         ActivateMenuLeft
     ]
 );
 
-#[cfg(not(target_os = "macos"))]
+/// Opens the named menu in the client-side application menu.
+///
+/// Does not apply to platform menu bars (e.g. on macOS).
 #[derive(Clone, Deserialize, JsonSchema, PartialEq, Default, Action)]
 #[action(namespace = app_menu)]
 pub struct OpenApplicationMenu(String);

crates/zed/Cargo.toml 🔗

@@ -15,10 +15,6 @@ tracy = ["ztracing/tracy"]
 
 [[bin]]
 name = "zed"
-path = "src/zed-main.rs"
-
-[lib]
-name = "zed"
 path = "src/main.rs"
 
 [dependencies]

crates/zed/src/main.rs 🔗

@@ -1,3 +1,6 @@
+// Disable command line from opening on release mode
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
 mod reliability;
 mod zed;
 
@@ -163,9 +166,9 @@ fn fail_to_open_window(e: anyhow::Error, _cx: &mut App) {
         .detach();
     }
 }
-pub static STARTUP_TIME: OnceLock<Instant> = OnceLock::new();
+static STARTUP_TIME: OnceLock<Instant> = OnceLock::new();
 
-pub fn main() {
+fn main() {
     STARTUP_TIME.get_or_init(|| Instant::now());
 
     #[cfg(unix)]
@@ -1301,7 +1304,7 @@ fn init_paths() -> HashMap<io::ErrorKind, Vec<&'static Path>> {
     })
 }
 
-pub fn stdout_is_a_pty() -> bool {
+fn stdout_is_a_pty() -> bool {
     std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && io::stdout().is_terminal()
 }
 
@@ -1547,14 +1550,14 @@ fn dump_all_gpui_actions() {
     struct ActionDef {
         name: &'static str,
         human_name: String,
-        aliases: &'static [&'static str],
+        deprecated_aliases: &'static [&'static str],
         documentation: Option<&'static str>,
     }
     let mut actions = gpui::generate_list_of_all_registered_actions()
         .map(|action| ActionDef {
             name: action.name,
             human_name: command_palette::humanize_action_name(action.name),
-            aliases: action.deprecated_aliases,
+            deprecated_aliases: action.deprecated_aliases,
             documentation: action.documentation,
         })
         .collect::<Vec<ActionDef>>();

crates/zed/src/zed-main.rs 🔗

@@ -1,8 +0,0 @@
-// Disable command line from opening on release mode
-#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
-
-pub fn main() {
-    // separated out so that the file containing the main function can be imported by other crates,
-    // while having all gpui resources that are registered in main (primarily actions) initialized
-    zed::main();
-}

crates/zed/src/zed.rs 🔗

@@ -4780,7 +4780,6 @@ mod tests {
                 "activity_indicator",
                 "agent",
                 "agents",
-                #[cfg(not(target_os = "macos"))]
                 "app_menu",
                 "assistant",
                 "assistant2",

script/generate-action-metadata 🔗

@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+
+echo "Generating action metadata..."
+cargo run -p zed -- --dump-all-actions > crates/docs_preprocessor/actions.json
+
+echo "Generated crates/docs_preprocessor/actions.json with $(grep -c '"name":' crates/docs_preprocessor/actions.json) actions"

tooling/xtask/src/tasks/workflows/run_tests.rs 🔗

@@ -448,6 +448,7 @@ fn check_docs() -> NamedJob {
                 lychee_link_check("./docs/src/**/*"), // check markdown links
             )
             .map(steps::install_linux_dependencies)
+            .add_step(steps::script("./script/generate-action-metadata"))
             .add_step(install_mdbook())
             .add_step(build_docs())
             .add_step(