785 lines · 28.3 KB · Rust
RawDownload
use anyhow::{Context, Result};use mdbook::BookItem;use mdbook::book::{Book, Chapter};use mdbook::preprocess::CmdPreprocessor;use regex::Regex;use settings::{KeymapFile, SettingsStore};use std::borrow::Cow;use std::collections::{HashMap, HashSet};use std::io::{self, Read};use std::process;use std::sync::{LazyLock, OnceLock}; static KEYMAP_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| {    load_keymap("keymaps/default-macos.json").expect("Failed to load MacOS keymap")}); static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {    load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")}); static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {    load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")}); static ALL_ACTIONS: LazyLock<ActionManifest> = LazyLock::new(load_all_actions); const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->"; fn main() -> Result<()> {    zlog::init();    zlog::init_output_stderr();    let args = std::env::args().skip(1).collect::<Vec<_>>();     match args.get(0).map(String::as_str) {        Some("supports") => {            let renderer = args.get(1).expect("Required argument");            let supported = renderer != "not-supported";            if supported {                process::exit(0);            } else {                process::exit(1);            }        }        Some("postprocess") => handle_postprocessing()?,        _ => handle_preprocessing()?,    }     Ok(())} #[derive(Debug, Clone, PartialEq, Eq, Hash)]enum PreprocessorError {    ActionNotFound {        action_name: String,    },    DeprecatedActionUsed {        used: String,        should_be: String,    },    InvalidFrontmatterLine(String),    InvalidSettingsJson {        file: std::path::PathBuf,        line: usize,        snippet: String,        error: String,    },} impl PreprocessorError {    fn new_for_not_found_action(action_name: String) -> Self {        for action in &ALL_ACTIONS.actions {            for alias in &action.deprecated_aliases {                if alias == action_name.as_str() {                    return PreprocessorError::DeprecatedActionUsed {                        used: action_name,                        should_be: action.name.to_string(),                    };                }            }        }        PreprocessorError::ActionNotFound { action_name }    }     fn new_for_invalid_settings_json(        chapter: &Chapter,        location: usize,        snippet: String,        error: String,    ) -> Self {        PreprocessorError::InvalidSettingsJson {            file: chapter.path.clone().expect("chapter has path"),            line: chapter.content[..location].lines().count() + 1,            snippet,            error,        }    }} impl std::fmt::Display for PreprocessorError {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        match self {            PreprocessorError::InvalidFrontmatterLine(line) => {                write!(f, "Invalid frontmatter line: {}", line)            }            PreprocessorError::ActionNotFound { action_name } => {                write!(f, "Action not found: {}", action_name)            }            PreprocessorError::DeprecatedActionUsed { used, should_be } => write!(                f,                "Deprecated action used: {} should be {}",                used, should_be            ),            PreprocessorError::InvalidSettingsJson {                file,                line,                snippet,                error,            } => {                write!(                    f,                    "Invalid settings JSON at {}:{}\nError: {}\n\n{}",                    file.display(),                    line,                    error,                    snippet                )            }        }    }} fn handle_preprocessing() -> Result<()> {    let mut stdin = io::stdin();    let mut input = String::new();    stdin.read_to_string(&mut input)?;     let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?;     let mut errors = HashSet::<PreprocessorError>::new();    handle_frontmatter(&mut book, &mut errors);    template_big_table_of_actions(&mut book);    template_and_validate_keybindings(&mut book, &mut errors);    template_and_validate_actions(&mut book, &mut errors);    template_and_validate_json_snippets(&mut book, &mut errors);     if !errors.is_empty() {        const ANSI_RED: &str = "\x1b[31m";        const ANSI_RESET: &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)?;     Ok(())} fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {    let frontmatter_regex = Regex::new(r"(?s)^\s*---(.*?)---").unwrap();    for_each_chapter_mut(book, |chapter| {        let new_content = frontmatter_regex.replace(&chapter.content, |caps: &regex::Captures| {            let frontmatter = caps[1].trim();            let frontmatter = frontmatter.trim_matches(&[' ', '-', '\n']);            let mut metadata = HashMap::<String, String>::default();            for line in frontmatter.lines() {                let Some((name, value)) = line.split_once(':') else {                    errors.insert(PreprocessorError::InvalidFrontmatterLine(format!(                        "{}: {}",                        chapter_breadcrumbs(chapter),                        line                    )));                    continue;                };                let name = name.trim();                let value = value.trim();                metadata.insert(name.to_string(), value.to_string());            }            FRONT_MATTER_COMMENT.replace(                "{}",                &serde_json::to_string(&metadata).expect("Failed to serialize metadata"),            )        });        if let Cow::Owned(content) = new_content {            chapter.content = content;        }    });} fn template_big_table_of_actions(book: &mut Book) {    for_each_chapter_mut(book, |chapter| {        let needle = "{#ACTIONS_TABLE#}";        if let Some(start) = chapter.content.rfind(needle) {            chapter.content.replace_range(                start..start + needle.len(),                &generate_big_table_of_actions(),            );        }    });} fn format_binding(binding: String) -> String {    binding.replace("\\", "\\\\")} fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {    let regex = Regex::new(r"\{#kb (.*?)\}").unwrap();     for_each_chapter_mut(book, |chapter| {        chapter.content = regex            .replace_all(&chapter.content, |caps: &regex::Captures| {                let action = caps[1].trim();                if is_missing_action(action) {                    errors.insert(PreprocessorError::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();                 if macos_binding.is_empty() && linux_binding.is_empty() {                    return "<div>No default binding</div>".to_string();                }                 let formatted_macos_binding = format_binding(macos_binding);                let formatted_linux_binding = format_binding(linux_binding);                 format!("<kbd class=\"keybinding\">{formatted_macos_binding}|{formatted_linux_binding}</kbd>")            })            .into_owned()    });} fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {    let regex = Regex::new(r"\{#action (.*?)\}").unwrap();     for_each_chapter_mut(book, |chapter| {        chapter.content = regex            .replace_all(&chapter.content, |caps: &regex::Captures| {                let name = caps[1].trim();                let Some(action) = find_action_by_name(name) else {                    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)            })            .into_owned()    });} fn find_action_by_name(name: &str) -> Option<&ActionDef> {    ALL_ACTIONS        .actions        .binary_search_by(|action| action.name.as_str().cmp(name))        .ok()        .map(|index| &ALL_ACTIONS.actions[index])} fn actions_available() -> bool {    !ALL_ACTIONS.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,        "linux" | "freebsd" => &KEYMAP_LINUX,        "windows" => &KEYMAP_WINDOWS,        _ => unreachable!("Not a valid OS: {}", os),    };     // Find the binding in reverse order, as the last binding takes precedence.    keymap.sections().rev().find_map(|section| {        section.bindings().rev().find_map(|(keystroke, a)| {            if name_for_action(a.to_string()) == action {                Some(keystroke.to_string())            } else {                None            }        })    })} fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {    let settings_schema = SettingsStore::json_schema(&Default::default());    let settings_validator = jsonschema::validator_for(&settings_schema)        .expect("failed to compile settings JSON schema");     let keymap_schema =        keymap_schema_for_actions(&ALL_ACTIONS.actions, &ALL_ACTIONS.schema_definitions);    let keymap_validator =        jsonschema::validator_for(&keymap_schema).expect("failed to compile keymap JSON schema");     fn for_each_labeled_code_block_mut(        book: &mut Book,        errors: &mut HashSet<PreprocessorError>,        f: &dyn Fn(&str, &str) -> anyhow::Result<()>,    ) {        const TAGGED_JSON_BLOCK_START: &'static str = "```json [";        const JSON_BLOCK_END: &'static str = "```";         for_each_chapter_mut(book, |chapter| {            let mut offset = 0;            while let Some(loc) = chapter.content[offset..].find(TAGGED_JSON_BLOCK_START) {                let loc = loc + offset;                let tag_start = loc + TAGGED_JSON_BLOCK_START.len();                offset = tag_start;                let Some(tag_end) = chapter.content[tag_start..].find(']') else {                    errors.insert(PreprocessorError::new_for_invalid_settings_json(                        chapter,                        loc,                        chapter.content[loc..tag_start].to_string(),                        "Unclosed JSON block tag".to_string(),                    ));                    continue;                };                let tag_end = tag_end + tag_start;                 let tag = &chapter.content[tag_start..tag_end];                 if tag.contains('\n') {                    errors.insert(PreprocessorError::new_for_invalid_settings_json(                        chapter,                        loc,                        chapter.content[loc..tag_start].to_string(),                        "Unclosed JSON block tag".to_string(),                    ));                    continue;                }                 let snippet_start = tag_end + 1;                offset = snippet_start;                 let Some(snippet_end) = chapter.content[snippet_start..].find(JSON_BLOCK_END)                else {                    errors.insert(PreprocessorError::new_for_invalid_settings_json(                        chapter,                        loc,                        chapter.content[loc..tag_end + 1].to_string(),                        "Missing closing code block".to_string(),                    ));                    continue;                };                let snippet_end = snippet_start + snippet_end;                let snippet_json = &chapter.content[snippet_start..snippet_end];                offset = snippet_end + 3;                 if let Err(err) = f(tag, snippet_json) {                    errors.insert(PreprocessorError::new_for_invalid_settings_json(                        chapter,                        loc,                        chapter.content[loc..snippet_end + 3].to_string(),                        err.to_string(),                    ));                    continue;                };                let tag_range_complete = tag_start - 1..tag_end + 1;                offset -= tag_range_complete.len();                chapter.content.replace_range(tag_range_complete, "");            }        });    }     for_each_labeled_code_block_mut(book, errors, &|label, snippet_json| {        let mut snippet_json_fixed = snippet_json            .to_string()            .replace("\n>", "\n")            .trim()            .to_string();        while snippet_json_fixed.starts_with("//") {            if let Some(line_end) = snippet_json_fixed.find('\n') {                snippet_json_fixed.replace_range(0..line_end, "");                snippet_json_fixed = snippet_json_fixed.trim().to_string();            }        }        match label {            "settings" => {                if !snippet_json_fixed.starts_with('{') || !snippet_json_fixed.ends_with('}') {                    snippet_json_fixed.insert(0, '{');                    snippet_json_fixed.push_str("\n}");                }                let value =                    settings::parse_json_with_comments::<serde_json::Value>(&snippet_json_fixed)?;                let validation_errors: Vec<String> = settings_validator                    .iter_errors(&value)                    .map(|err| err.to_string())                    .collect();                if !validation_errors.is_empty() {                    anyhow::bail!("{}", validation_errors.join("\n"));                }            }            "keymap" => {                if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {                    snippet_json_fixed.insert(0, '[');                    snippet_json_fixed.push_str("\n]");                }                 let value =                    settings::parse_json_with_comments::<serde_json::Value>(&snippet_json_fixed)?;                let validation_errors: Vec<String> = keymap_validator                    .iter_errors(&value)                    .map(|err| err.to_string())                    .collect();                if !validation_errors.is_empty() {                    anyhow::bail!("{}", validation_errors.join("\n"));                }            }            "debug" => {                if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {                    snippet_json_fixed.insert(0, '[');                    snippet_json_fixed.push_str("\n]");                }                 settings::parse_json_with_comments::<task::DebugTaskFile>(&snippet_json_fixed)?;            }            "tasks" => {                if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {                    snippet_json_fixed.insert(0, '[');                    snippet_json_fixed.push_str("\n]");                }                 settings::parse_json_with_comments::<task::TaskTemplates>(&snippet_json_fixed)?;            }            "icon-theme" => {                if !snippet_json_fixed.starts_with('{') || !snippet_json_fixed.ends_with('}') {                    snippet_json_fixed.insert(0, '{');                    snippet_json_fixed.push_str("\n}");                }                 settings::parse_json_with_comments::<theme::IconThemeFamilyContent>(                    &snippet_json_fixed,                )?;            }            "semantic_token_rules" => {                if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {                    snippet_json_fixed.insert(0, '[');                    snippet_json_fixed.push_str("\n]");                }                 settings::parse_json_with_comments::<settings::SemanticTokenRules>(                    &snippet_json_fixed,                )?;            }            label => anyhow::bail!("Unexpected JSON code block tag: {label}"),        };        Ok(())    });} /// Removes any configurable options from the stringified action if existing,/// ensuring that only the actual action name is returned. If the action consists/// only of a string and nothing else, the string is returned as-is.////// Example:////// This will return the action name unmodified.////// ```/// let action_as_str = "assistant::Assist";/// let action_name = name_for_action(action_as_str);/// assert_eq!(action_name, "assistant::Assist");/// ```////// This will return the action name with any trailing options removed.///////// ```/// let action_as_str = "\"editor::ToggleComments\", {\"advance_downwards\":false}";/// let action_name = name_for_action(action_as_str);/// assert_eq!(action_name, "editor::ToggleComments");/// ```fn name_for_action(action_as_str: String) -> String {    action_as_str        .split(",")        .next()        .map(|name| name.trim_matches('"').to_string())        .unwrap_or(action_as_str)} fn chapter_breadcrumbs(chapter: &Chapter) -> String {    let mut breadcrumbs = Vec::with_capacity(chapter.parent_names.len() + 1);    breadcrumbs.extend(chapter.parent_names.iter().map(String::as_str));    breadcrumbs.push(chapter.name.as_str());    format!("[{:?}] {}", chapter.source_path, breadcrumbs.join(" > "))} fn load_keymap(asset_path: &str) -> Result<KeymapFile> {    let content = util::asset_str::<settings::SettingsAssets>(asset_path);    KeymapFile::parse(content.as_ref())} 

Lines 1–500 of 785.