Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:30:36.726Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

main.rs

952 lines · 34.5 KB · rust
1use anyhow::{Context, Result};
2mod ai_discovery;
3
4use ai_discovery::{
5    add_markdown_alternate_link, docs_pages, rewrite_docs_links, write_ai_discovery_artifacts,
6    write_markdown_redirect_aliases, write_pages_redirects,
7};
8use mdbook::BookItem;
9use mdbook::book::{Book, Chapter};
10use mdbook::preprocess::CmdPreprocessor;
11use regex::Regex;
12use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
13use std::borrow::Cow;
14use std::collections::{HashMap, HashSet};
15use std::io::{self, Read};
16use std::process;
17use std::sync::{LazyLock, OnceLock};
18
19static KEYMAP_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| {
20    load_keymap("keymaps/default-macos.json").expect("Failed to load MacOS keymap")
21});
22
23static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
24    load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
25});
26
27static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
28    load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
29});
30
31static KEYMAP_JETBRAINS_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| {
32    load_keymap("keymaps/macos/jetbrains.json").expect("Failed to load JetBrains macOS keymap")
33});
34
35static KEYMAP_JETBRAINS_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
36    load_keymap("keymaps/linux/jetbrains.json").expect("Failed to load JetBrains Linux keymap")
37});
38
39static ALL_ACTIONS: LazyLock<ActionManifest> = LazyLock::new(load_all_actions);
40
41#[derive(Clone, Copy)]
42#[allow(dead_code)]
43enum Os {
44    MacOs,
45    Linux,
46    Windows,
47}
48
49#[derive(Clone, Copy)]
50enum KeymapOverlay {
51    JetBrains,
52}
53
54impl KeymapOverlay {
55    fn parse(name: &str) -> Option<Self> {
56        match name {
57            "jetbrains" => Some(Self::JetBrains),
58            _ => None,
59        }
60    }
61
62    fn keymap(self, os: Os) -> &'static KeymapFile {
63        match (self, os) {
64            (Self::JetBrains, Os::MacOs) => &KEYMAP_JETBRAINS_MACOS,
65            (Self::JetBrains, Os::Linux | Os::Windows) => &KEYMAP_JETBRAINS_LINUX,
66        }
67    }
68}
69
70const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
71
72fn main() -> Result<()> {
73    zlog::init();
74    zlog::init_output_stderr();
75    let args = std::env::args().skip(1).collect::<Vec<_>>();
76
77    match args.get(0).map(String::as_str) {
78        Some("supports") => {
79            let renderer = args.get(1).expect("Required argument");
80            let supported = renderer != "not-supported";
81            if supported {
82                process::exit(0);
83            } else {
84                process::exit(1);
85            }
86        }
87        Some("postprocess") => handle_postprocessing()?,
88        _ => handle_preprocessing()?,
89    }
90
91    Ok(())
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Hash)]
95enum PreprocessorError {
96    ActionNotFound {
97        action_name: String,
98    },
99    DeprecatedActionUsed {
100        used: String,
101        should_be: String,
102    },
103    InvalidFrontmatterLine(String),
104    InvalidSettingsJson {
105        file: std::path::PathBuf,
106        line: usize,
107        snippet: String,
108        error: String,
109    },
110    UnknownKeymapOverlay {
111        overlay_name: String,
112    },
113}
114
115impl PreprocessorError {
116    fn new_for_not_found_action(action_name: String) -> Self {
117        for action in &ALL_ACTIONS.actions {
118            for alias in &action.deprecated_aliases {
119                if alias == action_name.as_str() {
120                    return PreprocessorError::DeprecatedActionUsed {
121                        used: action_name,
122                        should_be: action.name.to_string(),
123                    };
124                }
125            }
126        }
127        PreprocessorError::ActionNotFound { action_name }
128    }
129
130    fn new_for_invalid_settings_json(
131        chapter: &Chapter,
132        location: usize,
133        snippet: String,
134        error: String,
135    ) -> Self {
136        PreprocessorError::InvalidSettingsJson {
137            file: chapter.path.clone().expect("chapter has path"),
138            line: chapter.content[..location].lines().count() + 1,
139            snippet,
140            error,
141        }
142    }
143}
144
145impl std::fmt::Display for PreprocessorError {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        match self {
148            PreprocessorError::InvalidFrontmatterLine(line) => {
149                write!(f, "Invalid frontmatter line: {}", line)
150            }
151            PreprocessorError::ActionNotFound { action_name } => {
152                write!(f, "Action not found: {}", action_name)
153            }
154            PreprocessorError::DeprecatedActionUsed { used, should_be } => write!(
155                f,
156                "Deprecated action used: {} should be {}",
157                used, should_be
158            ),
159            PreprocessorError::InvalidSettingsJson {
160                file,
161                line,
162                snippet,
163                error,
164            } => {
165                write!(
166                    f,
167                    "Invalid settings JSON at {}:{}\nError: {}\n\n{}",
168                    file.display(),
169                    line,
170                    error,
171                    snippet
172                )
173            }
174            PreprocessorError::UnknownKeymapOverlay { overlay_name } => {
175                write!(
176                    f,
177                    "Unknown keymap overlay: '{}'. Supported overlays: jetbrains",
178                    overlay_name
179                )
180            }
181        }
182    }
183}
184
185fn handle_preprocessing() -> Result<()> {
186    let mut stdin = io::stdin();
187    let mut input = String::new();
188    stdin.read_to_string(&mut input)?;
189
190    let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?;
191
192    let mut errors = HashSet::<PreprocessorError>::new();
193    handle_frontmatter(&mut book, &mut errors);
194    template_big_table_of_actions(&mut book);
195    template_and_validate_keybindings(&mut book, &mut errors);
196    template_and_validate_actions(&mut book, &mut errors);
197    template_and_validate_json_snippets(&mut book, &mut errors)?;
198
199    if !errors.is_empty() {
200        const ANSI_RED: &str = "\x1b[31m";
201        const ANSI_RESET: &str = "\x1b[0m";
202        for error in &errors {
203            eprintln!("{ANSI_RED}ERROR{ANSI_RESET}: {}", error);
204        }
205        return Err(anyhow::anyhow!("Found {} errors in docs", errors.len()));
206    }
207
208    serde_json::to_writer(io::stdout(), &book)?;
209
210    Ok(())
211}
212
213fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
214    let frontmatter_regex = Regex::new(r"(?s)^\s*---(.*?)---").unwrap();
215    for_each_chapter_mut(book, |chapter| {
216        let new_content = frontmatter_regex.replace(&chapter.content, |caps: &regex::Captures| {
217            let frontmatter = caps[1].trim();
218            let frontmatter = frontmatter.trim_matches(&[' ', '-', '\n']);
219            let mut metadata = HashMap::<String, String>::default();
220            for line in frontmatter.lines() {
221                let Some((name, value)) = line.split_once(':') else {
222                    errors.insert(PreprocessorError::InvalidFrontmatterLine(format!(
223                        "{}: {}",
224                        chapter_breadcrumbs(chapter),
225                        line
226                    )));
227                    continue;
228                };
229                let name = name.trim();
230                let value = value.trim();
231                metadata.insert(name.to_string(), value.to_string());
232            }
233            FRONT_MATTER_COMMENT.replace(
234                "{}",
235                &serde_json::to_string(&metadata).expect("Failed to serialize metadata"),
236            )
237        });
238        if let Cow::Owned(content) = new_content {
239            chapter.content = content;
240        }
241    });
242}
243
244fn template_big_table_of_actions(book: &mut Book) {
245    for_each_chapter_mut(book, |chapter| {
246        let needle = "{#ACTIONS_TABLE#}";
247        if let Some(start) = chapter.content.rfind(needle) {
248            chapter.content.replace_range(
249                start..start + needle.len(),
250                &generate_big_table_of_actions(),
251            );
252        }
253    });
254}
255
256fn format_binding(binding: String) -> String {
257    binding.replace("\\", "\\\\")
258}
259
260fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
261    let regex = Regex::new(r"(?s)\{#kb(?::(\w+))?\s+(.*?)\}").unwrap();
262
263    for_each_chapter_mut(book, |chapter| {
264        chapter.content = regex
265            .replace_all(&chapter.content, |caps: &regex::Captures| {
266                let overlay_name = caps.get(1).map(|m| m.as_str());
267                let action = caps[2].trim();
268
269                if is_missing_action(action) {
270                    errors.insert(PreprocessorError::new_for_not_found_action(
271                        action.to_string(),
272                    ));
273                    return String::new();
274                }
275
276                let overlay = if let Some(name) = overlay_name {
277                    let Some(overlay) = KeymapOverlay::parse(name) else {
278                        errors.insert(PreprocessorError::UnknownKeymapOverlay {
279                            overlay_name: name.to_string(),
280                        });
281                        return String::new();
282                    };
283                    Some(overlay)
284                } else {
285                    None
286                };
287
288                let macos_binding =
289                    find_binding_with_overlay(Os::MacOs, action, overlay)
290                        .unwrap_or_default();
291                let linux_binding =
292                    find_binding_with_overlay(Os::Linux, action, overlay)
293                        .unwrap_or_default();
294
295                if macos_binding.is_empty() && linux_binding.is_empty() {
296                    return "<div>No default binding</div>".to_string();
297                }
298
299                let formatted_macos_binding = format_binding(macos_binding);
300                let formatted_linux_binding = format_binding(linux_binding);
301
302                format!("<kbd class=\"keybinding\">{formatted_macos_binding}&#124;{formatted_linux_binding}</kbd>")
303            })
304            .into_owned()
305    });
306}
307
308fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
309    let regex = Regex::new(r"(?s)\{#action\s+(.*?)\}").unwrap();
310
311    for_each_chapter_mut(book, |chapter| {
312        chapter.content = regex
313            .replace_all(&chapter.content, |caps: &regex::Captures| {
314                let name = caps[1].trim();
315                let Some(action) = find_action_by_name(name) else {
316                    if actions_available() {
317                        errors.insert(PreprocessorError::new_for_not_found_action(
318                            name.to_string(),
319                        ));
320                    }
321                    return format!("<code class=\"hljs\">{}</code>", name);
322                };
323                format!("<code class=\"hljs\">{}</code>", &action.human_name)
324            })
325            .into_owned()
326    });
327}
328
329fn find_action_by_name(name: &str) -> Option<&ActionDef> {
330    ALL_ACTIONS
331        .actions
332        .binary_search_by(|action| action.name.as_str().cmp(name))
333        .ok()
334        .map(|index| &ALL_ACTIONS.actions[index])
335}
336
337fn actions_available() -> bool {
338    !ALL_ACTIONS.actions.is_empty()
339}
340
341fn is_missing_action(name: &str) -> bool {
342    actions_available() && find_action_by_name(name).is_none()
343}
344
345// Find the last binding (in keymap order) for the given action.
346// Exact action matches are preferred over parameterized variants.
347fn find_binding_in_keymap(keymap: &KeymapFile, action: &str) -> Option<String> {
348    let find = |predicate: &dyn Fn(&str) -> bool| {
349        keymap.sections().rev().find_map(|section| {
350            section.bindings().rev().find_map(|(keystroke, a)| {
351                if predicate(&a.to_string()) {
352                    Some(keystroke.to_string())
353                } else {
354                    None
355                }
356            })
357        })
358    };
359
360    // Look for exact match
361    if let Some(binding) = find(&|a| a == action) {
362        return Some(binding);
363    }
364
365    // Look for parameterized match
366    find(&|a| name_for_action(a.to_string()) == action)
367}
368
369fn find_binding(os: Os, action: &str) -> Option<String> {
370    let keymap = match os {
371        Os::MacOs => &KEYMAP_MACOS,
372        Os::Linux => &KEYMAP_LINUX,
373        Os::Windows => &KEYMAP_WINDOWS,
374    };
375    find_binding_in_keymap(keymap, action)
376}
377
378fn find_binding_with_overlay(
379    os: Os,
380    action: &str,
381    overlay: Option<KeymapOverlay>,
382) -> Option<String> {
383    overlay
384        .and_then(|overlay| find_binding_in_keymap(overlay.keymap(os), action))
385        .or_else(|| find_binding(os, action))
386}
387
388fn template_and_validate_json_snippets(
389    book: &mut Book,
390    errors: &mut HashSet<PreprocessorError>,
391) -> Result<()> {
392    let params = SettingsJsonSchemaParams {
393        language_names: &[],
394        font_names: &[],
395        theme_names: &[],
396        icon_theme_names: &[],
397        lsp_adapter_names: &[],
398        action_names: &[],
399        action_documentation: &HashMap::default(),
400        deprecations: &HashMap::default(),
401        deprecation_messages: &HashMap::default(),
402    };
403    let settings_schema = SettingsStore::json_schema(&params);
404    let settings_validator = jsonschema::validator_for(&settings_schema)
405        .context("failed to compile settings JSON schema")?;
406
407    // The keymap schema is built from the action manifest. When `actions.json`
408    // is unavailable (e.g. when running outside of CI without first generating
409    // it) there are no actions, which produces an invalid schema (an empty
410    // `anyOf`). In that case we skip keymap snippet validation rather than
411    // panicking, consistent with the action validation skipped above.
412    let keymap_validator = if actions_available() {
413        let keymap_schema =
414            keymap_schema_for_actions(&ALL_ACTIONS.actions, &ALL_ACTIONS.schema_definitions);
415        Some(
416            jsonschema::validator_for(&keymap_schema)
417                .context("failed to compile keymap JSON schema")?,
418        )
419    } else {
420        None
421    };
422
423    fn for_each_labeled_code_block_mut(
424        book: &mut Book,
425        errors: &mut HashSet<PreprocessorError>,
426        f: &dyn Fn(&str, &str) -> anyhow::Result<()>,
427    ) {
428        const TAGGED_JSON_BLOCK_START: &'static str = "```json [";
429        const JSON_BLOCK_END: &'static str = "```";
430
431        for_each_chapter_mut(book, |chapter| {
432            let mut offset = 0;
433            while let Some(loc) = chapter.content[offset..].find(TAGGED_JSON_BLOCK_START) {
434                let loc = loc + offset;
435                let tag_start = loc + TAGGED_JSON_BLOCK_START.len();
436                offset = tag_start;
437                let Some(tag_end) = chapter.content[tag_start..].find(']') else {
438                    errors.insert(PreprocessorError::new_for_invalid_settings_json(
439                        chapter,
440                        loc,
441                        chapter.content[loc..tag_start].to_string(),
442                        "Unclosed JSON block tag".to_string(),
443                    ));
444                    continue;
445                };
446                let tag_end = tag_end + tag_start;
447
448                let tag = &chapter.content[tag_start..tag_end];
449
450                if tag.contains('\n') {
451                    errors.insert(PreprocessorError::new_for_invalid_settings_json(
452                        chapter,
453                        loc,
454                        chapter.content[loc..tag_start].to_string(),
455                        "Unclosed JSON block tag".to_string(),
456                    ));
457                    continue;
458                }
459
460                let snippet_start = tag_end + 1;
461                offset = snippet_start;
462
463                let Some(snippet_end) = chapter.content[snippet_start..].find(JSON_BLOCK_END)
464                else {
465                    errors.insert(PreprocessorError::new_for_invalid_settings_json(
466                        chapter,
467                        loc,
468                        chapter.content[loc..tag_end + 1].to_string(),
469                        "Missing closing code block".to_string(),
470                    ));
471                    continue;
472                };
473                let snippet_end = snippet_start + snippet_end;
474                let snippet_json = &chapter.content[snippet_start..snippet_end];
475                offset = snippet_end + 3;
476
477                if let Err(err) = f(tag, snippet_json) {
478                    errors.insert(PreprocessorError::new_for_invalid_settings_json(
479                        chapter,
480                        loc,
481                        chapter.content[loc..snippet_end + 3].to_string(),
482                        err.to_string(),
483                    ));
484                    continue;
485                };
486                let tag_range_complete = tag_start - 1..tag_end + 1;
487                offset -= tag_range_complete.len();
488                chapter.content.replace_range(tag_range_complete, "");
489            }
490        });
491    }
492
493    for_each_labeled_code_block_mut(book, errors, &|label, snippet_json| {
494        let mut snippet_json_fixed = snippet_json
495            .to_string()
496            .replace("\n>", "\n")
497            .trim()
498            .to_string();
499        while snippet_json_fixed.starts_with("//") {
500            if let Some(line_end) = snippet_json_fixed.find('\n') {
501                snippet_json_fixed.replace_range(0..line_end, "");
502                snippet_json_fixed = snippet_json_fixed.trim().to_string();
503            }
504        }
505        match label {
506            "settings" => {
507                if !snippet_json_fixed.starts_with('{') || !snippet_json_fixed.ends_with('}') {
508                    snippet_json_fixed.insert(0, '{');
509                    snippet_json_fixed.push_str("\n}");
510                }
511                let value =
512                    settings::parse_json_with_comments::<serde_json::Value>(&snippet_json_fixed)?;
513                let validation_errors: Vec<String> = settings_validator
514                    .iter_errors(&value)
515                    .map(|err| err.to_string())
516                    .collect();
517                if !validation_errors.is_empty() {
518                    anyhow::bail!("{}", validation_errors.join("\n"));
519                }
520            }
521            "keymap" => {
522                if let Some(keymap_validator) = &keymap_validator {
523                    if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
524                        snippet_json_fixed.insert(0, '[');
525                        snippet_json_fixed.push_str("\n]");
526                    }
527
528                    let value = settings::parse_json_with_comments::<serde_json::Value>(
529                        &snippet_json_fixed,
530                    )?;
531                    let validation_errors: Vec<String> = keymap_validator
532                        .iter_errors(&value)
533                        .map(|err| err.to_string())
534                        .collect();
535                    if !validation_errors.is_empty() {
536                        anyhow::bail!("{}", validation_errors.join("\n"));
537                    }
538                }
539            }
540            "debug" => {
541                if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
542                    snippet_json_fixed.insert(0, '[');
543                    snippet_json_fixed.push_str("\n]");
544                }
545
546                settings::parse_json_with_comments::<task::DebugTaskFile>(&snippet_json_fixed)?;
547            }
548            "tasks" => {
549                if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
550                    snippet_json_fixed.insert(0, '[');
551                    snippet_json_fixed.push_str("\n]");
552                }
553
554                settings::parse_json_with_comments::<task::TaskTemplates>(&snippet_json_fixed)?;
555            }
556            "icon-theme" => {
557                if !snippet_json_fixed.starts_with('{') || !snippet_json_fixed.ends_with('}') {
558                    snippet_json_fixed.insert(0, '{');
559                    snippet_json_fixed.push_str("\n}");
560                }
561
562                settings::parse_json_with_comments::<theme::IconThemeFamilyContent>(
563                    &snippet_json_fixed,
564                )?;
565            }
566            "semantic_token_rules" => {
567                if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
568                    snippet_json_fixed.insert(0, '[');
569                    snippet_json_fixed.push_str("\n]");
570                }
571
572                settings::parse_json_with_comments::<settings::SemanticTokenRules>(
573                    &snippet_json_fixed,
574                )?;
575            }
576            label => anyhow::bail!("Unexpected JSON code block tag: {label}"),
577        };
578        Ok(())
579    });
580
581    Ok(())
582}
583
584/// Removes any configurable options from the stringified action if existing,
585/// ensuring that only the actual action name is returned. If the action consists
586/// only of a string and nothing else, the string is returned as-is.
587///
588/// Example:
589///
590/// This will return the action name unmodified.
591///
592/// ```
593/// let action_as_str = "workspace::Save";
594/// let action_name = name_for_action(action_as_str);
595/// assert_eq!(action_name, "workspace::Save");
596/// ```
597///
598/// This will return the action name with any trailing options removed.
599///
600///
601/// ```
602/// let action_as_str = "\"editor::ToggleComments\", {\"advance_downwards\":false}";
603/// let action_name = name_for_action(action_as_str);
604/// assert_eq!(action_name, "editor::ToggleComments");
605/// ```
606fn name_for_action(action_as_str: String) -> String {
607    action_as_str
608        .split(",")
609        .next()
610        .map(|name| name.trim_matches('"').to_string())
611        .unwrap_or(action_as_str)
612}
613
614fn chapter_breadcrumbs(chapter: &Chapter) -> String {
615    let mut breadcrumbs = Vec::with_capacity(chapter.parent_names.len() + 1);
616    breadcrumbs.extend(chapter.parent_names.iter().map(String::as_str));
617    breadcrumbs.push(chapter.name.as_str());
618    format!("[{:?}] {}", chapter.source_path, breadcrumbs.join(" > "))
619}
620
621fn load_keymap(asset_path: &str) -> Result<KeymapFile> {
622    let content = util::asset_str::<settings::SettingsAssets>(asset_path);
623    KeymapFile::parse(content.as_ref())
624}
625
626fn for_each_chapter_mut<F>(book: &mut Book, mut func: F)
627where
628    F: FnMut(&mut Chapter),
629{
630    book.for_each_mut(|item| {
631        let BookItem::Chapter(chapter) = item else {
632            return;
633        };
634        func(chapter);
635    });
636}
637
638#[derive(Debug, serde::Serialize, serde::Deserialize)]
639struct ActionDef {
640    name: String,
641    human_name: String,
642    #[serde(default)]
643    schema: Option<serde_json::Value>,
644    deprecated_aliases: Vec<String>,
645    #[serde(default)]
646    deprecation_message: Option<String>,
647    #[serde(rename = "documentation")]
648    docs: Option<String>,
649}
650
651#[derive(Debug, serde::Deserialize)]
652struct ActionManifest {
653    actions: Vec<ActionDef>,
654    #[serde(default)]
655    schema_definitions: serde_json::Map<String, serde_json::Value>,
656}
657
658fn load_all_actions() -> ActionManifest {
659    let asset_path = concat!(env!("CARGO_MANIFEST_DIR"), "/actions.json");
660    match std::fs::read_to_string(asset_path) {
661        Ok(content) => {
662            let mut manifest: ActionManifest =
663                serde_json::from_str(&content).expect("Failed to parse actions.json");
664            manifest.actions.sort_by(|a, b| a.name.cmp(&b.name));
665            manifest
666        }
667        Err(err) => {
668            if std::env::var("CI").is_ok() {
669                panic!("actions.json not found at {}: {}", asset_path, err);
670            }
671            eprintln!(
672                "Warning: actions.json not found, action validation will be skipped: {}",
673                err
674            );
675            ActionManifest {
676                actions: Vec::new(),
677                schema_definitions: serde_json::Map::new(),
678            }
679        }
680    }
681}
682
683fn handle_postprocessing() -> Result<()> {
684    let logger = zlog::scoped!("render");
685    let mut ctx = mdbook::renderer::RenderContext::from_json(io::stdin())?;
686    let output = ctx
687        .config
688        .get_mut("output")
689        .expect("has output")
690        .as_table_mut()
691        .expect("output is table");
692    let zed_html = output.remove("zed-html").expect("zed-html output defined");
693    let redirects = zed_html
694        .get("redirect")
695        .and_then(|redirects| redirects.as_table())
696        .map(|redirects| {
697            redirects
698                .iter()
699                .filter_map(|(source, destination)| {
700                    destination
701                        .as_str()
702                        .map(|destination| (source.clone(), destination.to_string()))
703                })
704                .collect::<Vec<_>>()
705        });
706    let default_description = zed_html
707        .get("default-description")
708        .expect("Default description not found")
709        .as_str()
710        .expect("Default description not a string")
711        .to_string();
712    let default_title = zed_html
713        .get("default-title")
714        .expect("Default title not found")
715        .as_str()
716        .expect("Default title not a string")
717        .to_string();
718    let amplitude_key = std::env::var("DOCS_AMPLITUDE_API_KEY").unwrap_or_default();
719    let consent_io_instance = std::env::var("DOCS_CONSENT_IO_INSTANCE").unwrap_or_default();
720    let docs_channel = std::env::var("DOCS_CHANNEL").unwrap_or_else(|_| "stable".to_string());
721    let site_url = std::env::var("MDBOOK_BOOK__SITE_URL")
722        .ok()
723        .filter(|site_url| !site_url.trim().is_empty())
724        .unwrap_or_else(|| {
725            match docs_channel.as_str() {
726                "nightly" => "/docs/nightly/",
727                "preview" => "/docs/preview/",
728                _ => "/docs/",
729            }
730            .to_string()
731        });
732    let noindex = if docs_channel == "nightly" || docs_channel == "preview" {
733        "<meta name=\"robots\" content=\"noindex, nofollow\">"
734    } else {
735        ""
736    };
737
738    output.insert("html".to_string(), zed_html);
739    mdbook::Renderer::render(&mdbook::renderer::HtmlHandlebars::new(), &ctx)?;
740    let ignore_list = ["toc.html"];
741
742    let root_dir = ctx.destination.clone();
743    let mut files = Vec::with_capacity(128);
744    let mut queue = Vec::with_capacity(64);
745    queue.push(root_dir.clone());
746    while let Some(dir) = queue.pop() {
747        for entry in std::fs::read_dir(&dir).context("failed to read docs dir")? {
748            let Ok(entry) = entry else {
749                continue;
750            };
751            let file_type = entry.file_type().context("Failed to determine file type")?;
752            if file_type.is_dir() {
753                queue.push(entry.path());
754            }
755            if file_type.is_file()
756                && matches!(
757                    entry.path().extension().and_then(std::ffi::OsStr::to_str),
758                    Some("html")
759                )
760            {
761                if ignore_list.contains(&&*entry.file_name().to_string_lossy()) {
762                    zlog::info!(logger => "Ignoring {}", entry.path().to_string_lossy());
763                } else {
764                    files.push(entry.path());
765                }
766            }
767        }
768    }
769
770    zlog::info!(logger => "Processing {} `.html` files", files.len());
771    let pages = docs_pages(&ctx.book)?;
772    write_ai_discovery_artifacts(&pages, &root_dir, &site_url)?;
773    let meta_regex = Regex::new(&FRONT_MATTER_COMMENT.replace("{}", "(.*)")).unwrap();
774    for file in &files {
775        let contents = std::fs::read_to_string(&file)?;
776        let mut meta_description = None;
777        let mut meta_title = None;
778        let contents = meta_regex.replace(&contents, |caps: &regex::Captures| {
779            let metadata: HashMap<String, String> = serde_json::from_str(&caps[1]).with_context(|| format!("JSON Metadata: {:?}", &caps[1])).expect("Failed to deserialize metadata");
780            for (kind, content) in metadata {
781                match kind.as_str() {
782                    "description" => {
783                        meta_description = Some(content);
784                    }
785                    "title" => {
786                        meta_title = Some(content);
787                    }
788                    _ => {
789                        zlog::warn!(logger => "Unrecognized frontmatter key: {} in {:?}", kind, pretty_path(&file, &root_dir));
790                    }
791                }
792            }
793            String::new()
794        });
795        let meta_description = meta_description.as_ref().unwrap_or_else(|| {
796            zlog::warn!(logger => "No meta description found for {:?}", pretty_path(&file, &root_dir));
797            &default_description
798        });
799        let page_title = extract_title_from_page(&contents, pretty_path(&file, &root_dir));
800        let meta_title = meta_title.as_ref().unwrap_or_else(|| {
801            zlog::debug!(logger => "No meta title found for {:?}", pretty_path(&file, &root_dir));
802            &default_title
803        });
804        let meta_title = format!("{} | {}", page_title, meta_title);
805        zlog::trace!(logger => "Updating {:?}", pretty_path(&file, &root_dir));
806        let contents = contents.replace("#description#", meta_description);
807        let contents = contents.replace("#amplitude_key#", &amplitude_key);
808        let contents = contents.replace("#consent_io_instance#", &consent_io_instance);
809        let contents = contents.replace("#noindex#", noindex);
810        let contents = rewrite_docs_links(&contents, &site_url);
811        let contents = add_markdown_alternate_link(&contents, file, &root_dir, &site_url);
812        let contents = title_regex()
813            .replace(&contents, |_: &regex::Captures| {
814                format!("<title>{}</title>", meta_title)
815            })
816            .to_string();
817        std::fs::write(file, contents)?;
818    }
819    if let Some(redirects) = redirects {
820        write_markdown_redirect_aliases(&root_dir, &redirects, &site_url)?;
821        write_pages_redirects(&root_dir, &redirects, &site_url)?;
822    }
823    return Ok(());
824
825    fn pretty_path<'a>(
826        path: &'a std::path::PathBuf,
827        root: &'a std::path::PathBuf,
828    ) -> &'a std::path::Path {
829        path.strip_prefix(&root).unwrap_or(path)
830    }
831    fn extract_title_from_page(contents: &str, pretty_path: &std::path::Path) -> String {
832        let title_tag_contents = &title_regex()
833            .captures(contents)
834            .with_context(|| format!("Failed to find title in {:?}", pretty_path))
835            .expect("Page has <title> element")[1];
836
837        title_tag_contents
838            .trim()
839            .strip_suffix("- Zed")
840            .unwrap_or(title_tag_contents)
841            .trim()
842            .to_string()
843    }
844}
845
846fn title_regex() -> &'static Regex {
847    static TITLE_REGEX: OnceLock<Regex> = OnceLock::new();
848    TITLE_REGEX.get_or_init(|| Regex::new(r"<title>\s*(.*?)\s*</title>").unwrap())
849}
850
851fn generate_big_table_of_actions() -> String {
852    let actions = &ALL_ACTIONS.actions;
853    let mut output = String::new();
854
855    let mut actions_sorted = actions.iter().collect::<Vec<_>>();
856    actions_sorted.sort_by_key(|a| a.name.as_str());
857
858    // Start the definition list with custom styling for better spacing
859    output.push_str("<dl style=\"line-height: 1.8;\">\n");
860
861    for action in actions_sorted.into_iter() {
862        // Add the humanized action name as the term with margin
863        output.push_str(
864            "<dt style=\"margin-top: 1.5em; margin-bottom: 0.5em; font-weight: bold;\"><code>",
865        );
866        output.push_str(&action.human_name);
867        output.push_str("</code></dt>\n");
868
869        // Add the definition with keymap name and description
870        output.push_str("<dd style=\"margin-left: 2em; margin-bottom: 1em;\">\n");
871
872        // Add the description, escaping HTML if needed
873        if let Some(description) = action.docs.as_ref() {
874            output.push_str(
875                &description
876                    .replace("&", "&amp;")
877                    .replace("<", "&lt;")
878                    .replace(">", "&gt;"),
879            );
880            output.push_str("<br>\n");
881        }
882        output.push_str("Keymap Name: <code>");
883        output.push_str(&action.name);
884        output.push_str("</code><br>\n");
885        if !action.deprecated_aliases.is_empty() {
886            output.push_str("Deprecated Alias(es): ");
887            for alias in action.deprecated_aliases.iter() {
888                output.push_str("<code>");
889                output.push_str(alias);
890                output.push_str("</code>, ");
891            }
892        }
893        output.push_str("\n</dd>\n");
894    }
895
896    // Close the definition list
897    output.push_str("</dl>\n");
898
899    output
900}
901
902fn keymap_schema_for_actions(
903    actions: &[ActionDef],
904    schema_definitions: &serde_json::Map<String, serde_json::Value>,
905) -> serde_json::Value {
906    let mut generator = KeymapFile::action_schema_generator();
907
908    for (name, definition) in schema_definitions {
909        generator
910            .definitions_mut()
911            .insert(name.clone(), definition.clone());
912    }
913
914    let mut action_schemas = Vec::new();
915    let mut documentation = collections::HashMap::<&str, &str>::default();
916    let mut deprecations = collections::HashMap::<&str, &str>::default();
917    let mut deprecation_messages = collections::HashMap::<&str, &str>::default();
918
919    for action in actions {
920        let schema = action
921            .schema
922            .as_ref()
923            .and_then(|v| serde_json::from_value::<schemars::Schema>(v.clone()).ok());
924        action_schemas.push((action.name.as_str(), schema));
925        if let Some(doc) = &action.docs {
926            documentation.insert(action.name.as_str(), doc.as_str());
927        }
928        if let Some(msg) = &action.deprecation_message {
929            deprecation_messages.insert(action.name.as_str(), msg.as_str());
930        }
931        for alias in &action.deprecated_aliases {
932            deprecations.insert(alias.as_str(), action.name.as_str());
933            let alias_schema = action
934                .schema
935                .as_ref()
936                .and_then(|v| serde_json::from_value::<schemars::Schema>(v.clone()).ok());
937            action_schemas.push((alias.as_str(), alias_schema));
938        }
939    }
940
941    KeymapFile::generate_json_schema(
942        generator,
943        action_schemas,
944        &documentation,
945        &deprecations,
946        &deprecation_messages,
947    )
948}
949
950#[cfg(test)]
951mod tests;
952
Served at tenant.openagents/omega Member data and write actions are omitted.