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](ec16e70336 (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
This commit is contained in:
parent
52770cd3ad
commit
17c3b741ec
14 changed files with 216 additions and 48 deletions
26
.github/actions/build_docs/action.yml
vendored
Normal file
26
.github/actions/build_docs/action.yml
vendored
Normal file
|
@ -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/
|
21
.github/workflows/ci.yml
vendored
21
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
19
.github/workflows/deploy_cloudflare.yml
vendored
19
.github/workflows/deploy_cloudflare.yml
vendored
|
@ -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
|
||||
|
|
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -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]]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<KeymapFile> = LazyLock::new(|| {
|
|||
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
|
||||
});
|
||||
|
||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = 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::<Error>::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<Error>) {
|
||||
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<Error>) {
|
||||
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::<String>()
|
||||
.trim()
|
||||
.to_string()
|
||||
.replace("::", ":");
|
||||
|
||||
format!("<code class=\"hljs\">{}</code>", 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!("<code class=\"hljs\">{}</code>", &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<String> {
|
||||
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<ActionDef> {
|
||||
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::<Vec<ActionDef>>();
|
||||
|
||||
actions.sort_by_key(|a| a.name);
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
|
|
@ -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<MacroActionData> {
|
||||
let mut actions = Vec::new();
|
||||
for builder in inventory::iter::<MacroActionBuilder> {
|
||||
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,
|
||||
|
|
|
@ -12,6 +12,10 @@ workspace = true
|
|||
|
||||
[[bin]]
|
||||
name = "zed"
|
||||
path = "src/zed-main.rs"
|
||||
|
||||
[lib]
|
||||
name = "zed"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
@ -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<io::ErrorKind, Vec<&'static Path>> {
|
|||
})
|
||||
}
|
||||
|
||||
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<String>,
|
||||
|
||||
/// 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<usize>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
dump_all_actions: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -1278,3 +1286,28 @@ fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>, cx: &m
|
|||
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn watch_languages(_fs: Arc<dyn fs::Fs>, _languages: Arc<LanguageRegistry>, _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::<Vec<ActionDef>>();
|
||||
|
||||
actions.sort_by_key(|a| a.name);
|
||||
|
||||
io::Write::write(
|
||||
&mut std::io::stdout(),
|
||||
serde_json::to_string_pretty(&actions).unwrap().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
|
5
crates/zed/src/zed-main.rs
Normal file
5
crates/zed/src/zed-main.rs
Normal file
|
@ -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();
|
||||
}
|
|
@ -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).
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue