From 17c3b741ecd8f13695cb21727f4c337f57412f23 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 4 Jun 2025 14:18:12 -0500 Subject: [PATCH] Validate actions in docs (#31073) Adds a validation step to docs preprocessing so that actions referenced in docs are checked against the list of all registered actions in GPUI. In order for this to work properly, all of the crates that register actions had to be importable by the `docs_preprocessor` crate and actually used (see [this comment](https://github.com/zed-industries/zed/commit/ec16e70336552255adf99671ca4d3c4e3d1b5c5d#diff-2674caf14ae6d70752ea60c7061232393d84e7f61a52915ace089c30a797a1c3) for why this is challenging). In order to accomplish this I have moved the entry point of zed into a separate stub file named `zed_main.rs` so that `main.rs` is importable by the `docs_preprocessor` crate, this is kind of gross, but ensures that all actions that are registered in the application are registered when checking them in `docs_preprocessor`. An alternative solution suggested by @mikayla-maki was to separate out all our `::init()` functions into a lib entry point in the `zed` crate that can be imported instead, however, this turned out to be a far bigger refactor and is in my opinion better to do in a follow up PR with significant testing to ensure no regressions in behavior occur. Release Notes: - N/A --- .github/actions/build_docs/action.yml | 26 ++++ .github/workflows/ci.yml | 21 ++++ .github/workflows/deploy_cloudflare.yml | 19 +-- Cargo.lock | 3 + assets/keymaps/default-linux.json | 2 +- assets/keymaps/default-macos.json | 2 +- crates/command_palette/src/command_palette.rs | 2 +- crates/docs_preprocessor/Cargo.toml | 3 + crates/docs_preprocessor/src/main.rs | 117 ++++++++++++++---- crates/gpui/src/action.rs | 13 +- crates/zed/Cargo.toml | 4 + crates/zed/src/main.rs | 45 ++++++- crates/zed/src/zed-main.rs | 5 + docs/src/git.md | 2 +- 14 files changed, 216 insertions(+), 48 deletions(-) create mode 100644 .github/actions/build_docs/action.yml create mode 100644 crates/zed/src/zed-main.rs diff --git a/.github/actions/build_docs/action.yml b/.github/actions/build_docs/action.yml new file mode 100644 index 0000000000..27f0f37d4f --- /dev/null +++ b/.github/actions/build_docs/action.yml @@ -0,0 +1,26 @@ +name: "Build docs" +description: "Build the docs" + +runs: + using: "composite" + steps: + - name: Setup mdBook + uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2 + with: + mdbook-version: "0.4.37" + + - name: Cache dependencies + uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 + with: + save-if: ${{ github.ref == 'refs/heads/main' }} + cache-provider: "buildjet" + + - name: Install Linux dependencies + shell: bash -euxo pipefail {0} + run: ./script/linux + + - name: Build book + shell: bash -euxo pipefail {0} + run: | + mkdir -p target/deploy + mdbook build ./docs --dest-dir=../target/deploy/docs/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f9414d2ea..c154505811 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,6 +191,27 @@ jobs: with: config: ./typos.toml + check_docs: + timeout-minutes: 60 + name: Check docs + needs: [job_spec] + if: github.repository_owner == 'zed-industries' + runs-on: + - buildjet-8vcpu-ubuntu-2204 + steps: + - name: Checkout repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + clean: false + + - name: Configure CI + run: | + mkdir -p ./../.cargo + cp ./.cargo/ci-config.toml ./../.cargo/config.toml + + - name: Build docs + uses: ./.github/actions/build_docs + macos_tests: timeout-minutes: 60 name: (macOS) Run Clippy and tests diff --git a/.github/workflows/deploy_cloudflare.yml b/.github/workflows/deploy_cloudflare.yml index 9222228d78..fe443d493e 100644 --- a/.github/workflows/deploy_cloudflare.yml +++ b/.github/workflows/deploy_cloudflare.yml @@ -9,7 +9,7 @@ jobs: deploy-docs: name: Deploy Docs if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest + runs-on: buildjet-16vcpu-ubuntu-2204 steps: - name: Checkout repo @@ -17,24 +17,11 @@ jobs: with: clean: false - - name: Setup mdBook - uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2 - with: - mdbook-version: "0.4.37" - - name: Set up default .cargo/config.toml run: cp ./.cargo/collab-config.toml ./.cargo/config.toml - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install libxkbcommon-dev libxkbcommon-x11-dev - - - name: Build book - run: | - set -euo pipefail - mkdir -p target/deploy - mdbook build ./docs --dest-dir=../target/deploy/docs/ + - name: Build docs + uses: ./.github/actions/build_docs - name: Deploy Docs uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3 diff --git a/Cargo.lock b/Cargo.lock index 288bc81a57..07f629a653 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4543,6 +4543,8 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "command_palette", + "gpui", "mdbook", "regex", "serde", @@ -4550,6 +4552,7 @@ dependencies = [ "settings", "util", "workspace-hack", + "zed", ] [[package]] diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index f7dd30012b..1d0972c92f 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -120,7 +120,7 @@ "ctrl-'": "editor::ToggleSelectedDiffHunks", "ctrl-\"": "editor::ExpandAllDiffHunks", "ctrl-i": "editor::ShowSignatureHelp", - "alt-g b": "editor::ToggleGitBlame", + "alt-g b": "git::Blame", "menu": "editor::OpenContextMenu", "shift-f10": "editor::OpenContextMenu", "ctrl-shift-e": "editor::ToggleEditPrediction", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 8e3e895d11..833547ea6b 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -138,7 +138,7 @@ "cmd-;": "editor::ToggleLineNumbers", "cmd-'": "editor::ToggleSelectedDiffHunks", "cmd-\"": "editor::ExpandAllDiffHunks", - "cmd-alt-g b": "editor::ToggleGitBlame", + "cmd-alt-g b": "git::Blame", "cmd-i": "editor::ShowSignatureHelp", "f9": "editor::ToggleBreakpoint", "shift-f9": "editor::EditLogBreakpoint", diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 9c88af9d16..bafe611791 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -448,7 +448,7 @@ impl PickerDelegate for CommandPaletteDelegate { } } -fn humanize_action_name(name: &str) -> String { +pub fn humanize_action_name(name: &str) -> String { let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count(); let mut result = String::with_capacity(capacity); for char in name.chars() { diff --git a/crates/docs_preprocessor/Cargo.toml b/crates/docs_preprocessor/Cargo.toml index a77965ce1d..a0df669abe 100644 --- a/crates/docs_preprocessor/Cargo.toml +++ b/crates/docs_preprocessor/Cargo.toml @@ -15,6 +15,9 @@ settings.workspace = true regex.workspace = true util.workspace = true workspace-hack.workspace = true +zed.workspace = true +gpui.workspace = true +command_palette.workspace = true [lints] workspace = true diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index a6962e9bb0..c76ffd52a5 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -5,6 +5,7 @@ use mdbook::book::{Book, Chapter}; use mdbook::preprocess::CmdPreprocessor; use regex::Regex; use settings::KeymapFile; +use std::collections::HashSet; use std::io::{self, Read}; use std::process; use std::sync::LazyLock; @@ -17,6 +18,8 @@ static KEYMAP_LINUX: LazyLock = LazyLock::new(|| { load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap") }); +static ALL_ACTIONS: LazyLock> = LazyLock::new(dump_all_gpui_actions); + pub fn make_app() -> Command { Command::new("zed-docs-preprocessor") .about("Preprocesses Zed Docs content to provide rich action & keybinding support and more") @@ -29,6 +32,9 @@ pub fn make_app() -> Command { fn main() -> Result<()> { let matches = make_app().get_matches(); + // 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(); if let Some(sub_args) = matches.subcommand_matches("supports") { handle_supports(sub_args); @@ -39,6 +45,43 @@ fn main() -> Result<()> { Ok(()) } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum Error { + ActionNotFound { action_name: String }, + DeprecatedActionUsed { used: String, should_be: String }, +} + +impl Error { + 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 { + return Error::DeprecatedActionUsed { + used: action_name.clone(), + should_be: action.name.to_string(), + }; + } + } + } + Error::ActionNotFound { + action_name: action_name.to_string(), + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ActionNotFound { action_name } => write!(f, "Action not found: {}", action_name), + Error::DeprecatedActionUsed { used, should_be } => write!( + f, + "Deprecated action used: {} should be {}", + used, should_be + ), + } + } +} + fn handle_preprocessing() -> Result<()> { let mut stdin = io::stdin(); let mut input = String::new(); @@ -46,8 +89,19 @@ fn handle_preprocessing() -> Result<()> { let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?; - template_keybinding(&mut book); - template_action(&mut book); + let mut errors = HashSet::::new(); + + template_and_validate_keybindings(&mut book, &mut errors); + template_and_validate_actions(&mut book, &mut errors); + + if !errors.is_empty() { + const ANSI_RED: &'static str = "\x1b[31m"; + const ANSI_RESET: &'static str = "\x1b[0m"; + for error in &errors { + eprintln!("{ANSI_RED}ERROR{ANSI_RESET}: {}", error); + } + return Err(anyhow::anyhow!("Found {} errors in docs", errors.len())); + } serde_json::to_writer(io::stdout(), &book)?; @@ -66,13 +120,17 @@ fn handle_supports(sub_args: &ArgMatches) -> ! { } } -fn template_keybinding(book: &mut Book) { +fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet) { let regex = Regex::new(r"\{#kb (.*?)\}").unwrap(); for_each_chapter_mut(book, |chapter| { chapter.content = regex .replace_all(&chapter.content, |caps: ®ex::Captures| { let action = caps[1].trim(); + if find_action_by_name(action).is_none() { + errors.insert(Error::new_for_not_found_action(action.to_string())); + return String::new(); + } let macos_binding = find_binding("macos", action).unwrap_or_default(); let linux_binding = find_binding("linux", action).unwrap_or_default(); @@ -86,35 +144,30 @@ fn template_keybinding(book: &mut Book) { }); } -fn template_action(book: &mut Book) { +fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet) { let regex = Regex::new(r"\{#action (.*?)\}").unwrap(); for_each_chapter_mut(book, |chapter| { chapter.content = regex .replace_all(&chapter.content, |caps: ®ex::Captures| { let name = caps[1].trim(); - - let formatted_name = name - .chars() - .enumerate() - .map(|(i, c)| { - if i > 0 && c.is_uppercase() { - format!(" {}", c.to_lowercase()) - } else { - c.to_string() - } - }) - .collect::() - .trim() - .to_string() - .replace("::", ":"); - - format!("{}", formatted_name) + let Some(action) = find_action_by_name(name) else { + errors.insert(Error::new_for_not_found_action(name.to_string())); + return String::new(); + }; + format!("{}", &action.human_name) }) .into_owned() }); } +fn find_action_by_name(name: &str) -> Option<&ActionDef> { + ALL_ACTIONS + .binary_search_by(|action| action.name.cmp(name)) + .ok() + .map(|index| &ALL_ACTIONS[index]) +} + fn find_binding(os: &str, action: &str) -> Option { let keymap = match os { "macos" => &KEYMAP_MACOS, @@ -180,3 +233,25 @@ where func(chapter); }); } + +#[derive(Debug, serde::Serialize)] +struct ActionDef { + name: &'static str, + human_name: String, + deprecated_aliases: &'static [&'static str], +} + +fn dump_all_gpui_actions() -> Vec { + let mut actions = gpui::generate_list_of_all_registered_actions() + .into_iter() + .map(|action| ActionDef { + name: action.name, + human_name: command_palette::humanize_action_name(action.name), + deprecated_aliases: action.aliases, + }) + .collect::>(); + + actions.sort_by_key(|a| a.name); + + return actions; +} diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index d7b97ce91d..db617758b3 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -288,6 +288,18 @@ impl ActionRegistry { } } +/// Generate a list of all the registered actions. +/// Useful for transforming the list of available actions into a +/// format suited for static analysis such as in validating keymaps, or +/// generating documentation. +pub fn generate_list_of_all_registered_actions() -> Vec { + let mut actions = Vec::new(); + for builder in inventory::iter:: { + actions.push(builder.0()); + } + actions +} + /// Defines and registers unit structs that can be used as actions. /// /// To use more complex data types as actions, use `impl_actions!` @@ -333,7 +345,6 @@ macro_rules! action_as { ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq, )] pub struct $name; - gpui::__impl_action!( $namespace, $name, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 9c7a1c554a..c40ea4cb98 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -12,6 +12,10 @@ workspace = true [[bin]] name = "zed" +path = "src/zed-main.rs" + +[lib] +name = "zed" path = "src/main.rs" [dependencies] diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 04bd9b7140..532963fadf 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -163,7 +163,7 @@ fn fail_to_open_window(e: anyhow::Error, _cx: &mut App) { } } -fn main() { +pub fn main() { #[cfg(unix)] { let is_root = nix::unistd::geteuid().is_root(); @@ -199,6 +199,11 @@ Error: Running Zed as root or via sudo is unsupported. return; } + if args.dump_all_actions { + dump_all_gpui_actions(); + return; + } + // Set custom data directory. if let Some(dir) = &args.user_data_dir { paths::set_custom_data_dir(dir); @@ -213,9 +218,6 @@ Error: Running Zed as root or via sudo is unsupported. } } - menu::init(); - zed_actions::init(); - let file_errors = init_paths(); if !file_errors.is_empty() { files_not_created_on_launch(file_errors); @@ -356,6 +358,9 @@ Error: Running Zed as root or via sudo is unsupported. }); app.run(move |cx| { + menu::init(); + zed_actions::init(); + release_channel::init(app_version, cx); gpui_tokio::init(cx); if let Some(app_commit_sha) = app_commit_sha { @@ -1018,7 +1023,7 @@ fn init_paths() -> HashMap> { }) } -fn stdout_is_a_pty() -> bool { +pub fn stdout_is_a_pty() -> bool { std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && io::stdout().is_terminal() } @@ -1055,7 +1060,7 @@ struct Args { #[arg(long, hide = true)] askpass: Option, - /// Run zed in the foreground, only used on Windows, to match the behavior of the behavior on macOS. + /// Run zed in the foreground, only used on Windows, to match the behavior on macOS. #[arg(long)] #[cfg(target_os = "windows")] #[arg(hide = true)] @@ -1066,6 +1071,9 @@ struct Args { #[cfg(target_os = "windows")] #[arg(hide = true)] dock_action: Option, + + #[arg(long, hide = true)] + dump_all_actions: bool, } #[derive(Clone, Debug)] @@ -1278,3 +1286,28 @@ fn watch_languages(fs: Arc, languages: Arc, cx: &m #[cfg(not(debug_assertions))] fn watch_languages(_fs: Arc, _languages: Arc, _cx: &mut App) {} + +fn dump_all_gpui_actions() { + #[derive(Debug, serde::Serialize)] + struct ActionDef { + name: &'static str, + human_name: String, + aliases: &'static [&'static str], + } + let mut actions = gpui::generate_list_of_all_registered_actions() + .into_iter() + .map(|action| ActionDef { + name: action.name, + human_name: command_palette::humanize_action_name(action.name), + aliases: action.aliases, + }) + .collect::>(); + + actions.sort_by_key(|a| a.name); + + io::Write::write( + &mut std::io::stdout(), + serde_json::to_string_pretty(&actions).unwrap().as_bytes(), + ) + .unwrap(); +} diff --git a/crates/zed/src/zed-main.rs b/crates/zed/src/zed-main.rs new file mode 100644 index 0000000000..051d02802e --- /dev/null +++ b/crates/zed/src/zed-main.rs @@ -0,0 +1,5 @@ +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(); +} diff --git a/docs/src/git.md b/docs/src/git.md index a7dcfbefe2..69d87ddf66 100644 --- a/docs/src/git.md +++ b/docs/src/git.md @@ -120,7 +120,7 @@ or by simply right clicking and selecting `Copy Permalink` with line(s) selected | {#action git::Branch} | {#kb git::Branch} | | {#action git::Switch} | {#kb git::Switch} | | {#action git::CheckoutBranch} | {#kb git::CheckoutBranch} | -| {#action editor::ToggleGitBlame} | {#kb editor::ToggleGitBlame} | +| {#action git::Blame} | {#kb git::Blame} | | {#action editor::ToggleGitBlameInline} | {#kb editor::ToggleGitBlameInline} | > Not all actions have default keybindings, but can be bound by [customizing your keymap](./key-bindings.md#user-keymaps).