lsp-docs 0.1.1

Static structured documentation for the Axon language, embedded at compile time. Consumed by axon-lsp's hover and completion resolvers.
Documentation
//! `lsp-docs` — static structured docs for the Axon language.
//!
//! Build-time embedder ([`build.rs`](../build.rs)) walks
//! `content/{types,syntax,handlers}/*.md`, parses each file's
//! frontmatter, and emits `OUT_DIR/generated.rs`. At runtime this
//! crate exposes pre-allocated entries through [`find_doc`] and
//! [`find_any_doc`] — no disk I/O, no allocation per lookup, and
//! `cargo tree -p lsp-docs --edges normal` stays empty.
//!
//! Consumed by `lsp-core::hover` (rich Markdown for built-in types
//! and syntax keywords) and `lsp-core::completion` (decorating
//! `CompletionItem.documentation` with the same Markdown).

#![allow(clippy::module_name_repetitions)]

/// Topical bucket for a documentation entry.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DocKind {
    /// Built-in types (`String`, `Integer`, `Channel`, `Trusted`,
    /// …) — surfaced both on hover and inside type-annotation
    /// completion.
    Type,
    /// Declaration / step / clause keywords (`type`, `flow`,
    /// `persona`, `step`, `ask`, …) — surfaced on hover when the
    /// user lands on the keyword itself.
    Syntax,
    /// Stdlib handler names. The `0.g` shipped corpus does not
    /// populate this bucket yet (Axon's "handlers" are step kinds
    /// and field-level keys, mostly covered under `Syntax`); this
    /// variant exists so future content additions don't require a
    /// schema change.
    Handler,
}

/// Stability tier surfaced in the rendered hover header.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stability {
    Stable,
    Experimental,
}

impl Stability {
    #[must_use]
    pub const fn label(self) -> &'static str {
        match self {
            Self::Stable => "stable",
            Self::Experimental => "experimental",
        }
    }
}

/// One documentation entry, generated at build time.
#[derive(Debug, Clone, Copy)]
pub struct DocEntry {
    pub name: &'static str,
    pub kind: DocKind,
    pub since: &'static str,
    pub stability: Stability,
    /// Markdown body, ready to embed into a hover panel.
    pub body: &'static str,
}

include!(concat!(env!("OUT_DIR"), "/generated.rs"));

/// Look up a documentation entry by `(name, kind)`.
#[must_use]
pub fn find_doc(name: &str, kind: DocKind) -> Option<&'static DocEntry> {
    ENTRIES.iter().find(|e| e.name == name && e.kind == kind)
}

/// Look up the first entry with a given `name`, regardless of kind.
/// Hover uses this when it doesn't yet know whether the symbol is a
/// type or a syntax keyword.
#[must_use]
pub fn find_any_doc(name: &str) -> Option<&'static DocEntry> {
    ENTRIES.iter().find(|e| e.name == name)
}

/// Number of entries shipped in this build. Useful for smoke tests
/// (e.g., "we shipped at least N entries").
#[must_use]
pub const fn entry_count() -> usize {
    ENTRIES.len()
}

/// Iterate every entry. Order is stable: `(kind, name)` ascending.
pub fn iter_entries() -> impl Iterator<Item = &'static DocEntry> {
    ENTRIES.iter()
}

/// Render a Markdown header summarising `entry`'s frontmatter,
/// followed by the body. Used by hover and completion.
#[must_use]
pub fn render_doc(entry: &DocEntry) -> String {
    let kind_label = match entry.kind {
        DocKind::Type => "type",
        DocKind::Syntax => "syntax",
        DocKind::Handler => "handler",
    };
    format!(
        "**{kind}** `{name}` · {stability} · since {since}\n\n---\n\n{body}",
        kind = kind_label,
        name = entry.name,
        stability = entry.stability.label(),
        since = entry.since,
        body = entry.body,
    )
}

#[cfg(test)]
mod tests {
    use super::{
        DocKind, Stability, entry_count, find_any_doc, find_doc, iter_entries, render_doc,
    };

    #[test]
    fn corpus_is_non_empty() {
        assert!(
            entry_count() >= 10,
            "expected at least 10 doc entries (sub-fase 0.g verification floor); got {}",
            entry_count(),
        );
    }

    #[test]
    fn find_doc_pins_name_and_kind() {
        let s = find_doc("String", DocKind::Type).expect("String type doc");
        assert_eq!(s.name, "String");
        assert_eq!(s.kind, DocKind::Type);
        assert_eq!(s.stability, Stability::Stable);
        assert!(!s.body.is_empty());
        // Wrong kind returns None.
        assert!(find_doc("String", DocKind::Syntax).is_none());
    }

    #[test]
    fn find_any_doc_falls_through_kinds() {
        // `flow` is a syntax keyword; find_any_doc finds it without
        // requiring the caller to know the kind upfront.
        let entry = find_any_doc("flow").expect("flow syntax doc");
        assert_eq!(entry.kind, DocKind::Syntax);
    }

    #[test]
    fn render_doc_includes_metadata_header() {
        let s = find_doc("String", DocKind::Type).unwrap();
        let md = render_doc(s);
        assert!(md.starts_with("**type** `String`"));
        assert!(md.contains("· stable"));
        assert!(md.contains("· since"));
        assert!(md.contains(s.body));
    }

    #[test]
    fn iter_entries_yields_stable_order() {
        let kinds: Vec<DocKind> = iter_entries().map(|e| e.kind).collect();
        // Sorted by (kind, name) — types come before syntax (since
        // DocKind::Type < DocKind::Syntax in the enum declaration
        // order).
        let mut sorted = kinds.clone();
        sorted.sort();
        assert_eq!(kinds, sorted, "iter_entries must yield stable order");
    }
}