Skip to main content

lsp_docs/
lib.rs

1//! `lsp-docs` — static structured docs for the Axon language.
2//!
3//! Build-time embedder ([`build.rs`](../build.rs)) walks
4//! `content/{types,syntax,handlers}/*.md`, parses each file's
5//! frontmatter, and emits `OUT_DIR/generated.rs`. At runtime this
6//! crate exposes pre-allocated entries through [`find_doc`] and
7//! [`find_any_doc`] — no disk I/O, no allocation per lookup, and
8//! `cargo tree -p lsp-docs --edges normal` stays empty.
9//!
10//! Consumed by `lsp-core::hover` (rich Markdown for built-in types
11//! and syntax keywords) and `lsp-core::completion` (decorating
12//! `CompletionItem.documentation` with the same Markdown).
13
14#![allow(clippy::module_name_repetitions)]
15
16/// Topical bucket for a documentation entry.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18pub enum DocKind {
19    /// Built-in types (`String`, `Integer`, `Channel`, `Trusted`,
20    /// …) — surfaced both on hover and inside type-annotation
21    /// completion.
22    Type,
23    /// Declaration / step / clause keywords (`type`, `flow`,
24    /// `persona`, `step`, `ask`, …) — surfaced on hover when the
25    /// user lands on the keyword itself.
26    Syntax,
27    /// Stdlib handler names. The `0.g` shipped corpus does not
28    /// populate this bucket yet (Axon's "handlers" are step kinds
29    /// and field-level keys, mostly covered under `Syntax`); this
30    /// variant exists so future content additions don't require a
31    /// schema change.
32    Handler,
33}
34
35/// Stability tier surfaced in the rendered hover header.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Stability {
38    Stable,
39    Experimental,
40}
41
42impl Stability {
43    #[must_use]
44    pub const fn label(self) -> &'static str {
45        match self {
46            Self::Stable => "stable",
47            Self::Experimental => "experimental",
48        }
49    }
50}
51
52/// One documentation entry, generated at build time.
53#[derive(Debug, Clone, Copy)]
54pub struct DocEntry {
55    pub name: &'static str,
56    pub kind: DocKind,
57    pub since: &'static str,
58    pub stability: Stability,
59    /// Markdown body, ready to embed into a hover panel.
60    pub body: &'static str,
61}
62
63include!(concat!(env!("OUT_DIR"), "/generated.rs"));
64
65/// Look up a documentation entry by `(name, kind)`.
66#[must_use]
67pub fn find_doc(name: &str, kind: DocKind) -> Option<&'static DocEntry> {
68    ENTRIES.iter().find(|e| e.name == name && e.kind == kind)
69}
70
71/// Look up the first entry with a given `name`, regardless of kind.
72/// Hover uses this when it doesn't yet know whether the symbol is a
73/// type or a syntax keyword.
74#[must_use]
75pub fn find_any_doc(name: &str) -> Option<&'static DocEntry> {
76    ENTRIES.iter().find(|e| e.name == name)
77}
78
79/// Number of entries shipped in this build. Useful for smoke tests
80/// (e.g., "we shipped at least N entries").
81#[must_use]
82pub const fn entry_count() -> usize {
83    ENTRIES.len()
84}
85
86/// Iterate every entry. Order is stable: `(kind, name)` ascending.
87pub fn iter_entries() -> impl Iterator<Item = &'static DocEntry> {
88    ENTRIES.iter()
89}
90
91/// Render a Markdown header summarising `entry`'s frontmatter,
92/// followed by the body. Used by hover and completion.
93#[must_use]
94pub fn render_doc(entry: &DocEntry) -> String {
95    let kind_label = match entry.kind {
96        DocKind::Type => "type",
97        DocKind::Syntax => "syntax",
98        DocKind::Handler => "handler",
99    };
100    format!(
101        "**{kind}** `{name}` · {stability} · since {since}\n\n---\n\n{body}",
102        kind = kind_label,
103        name = entry.name,
104        stability = entry.stability.label(),
105        since = entry.since,
106        body = entry.body,
107    )
108}
109
110#[cfg(test)]
111mod tests {
112    use super::{
113        DocKind, Stability, entry_count, find_any_doc, find_doc, iter_entries, render_doc,
114    };
115
116    #[test]
117    fn corpus_is_non_empty() {
118        assert!(
119            entry_count() >= 10,
120            "expected at least 10 doc entries (sub-fase 0.g verification floor); got {}",
121            entry_count(),
122        );
123    }
124
125    #[test]
126    fn find_doc_pins_name_and_kind() {
127        let s = find_doc("String", DocKind::Type).expect("String type doc");
128        assert_eq!(s.name, "String");
129        assert_eq!(s.kind, DocKind::Type);
130        assert_eq!(s.stability, Stability::Stable);
131        assert!(!s.body.is_empty());
132        // Wrong kind returns None.
133        assert!(find_doc("String", DocKind::Syntax).is_none());
134    }
135
136    #[test]
137    fn find_any_doc_falls_through_kinds() {
138        // `flow` is a syntax keyword; find_any_doc finds it without
139        // requiring the caller to know the kind upfront.
140        let entry = find_any_doc("flow").expect("flow syntax doc");
141        assert_eq!(entry.kind, DocKind::Syntax);
142    }
143
144    #[test]
145    fn render_doc_includes_metadata_header() {
146        let s = find_doc("String", DocKind::Type).unwrap();
147        let md = render_doc(s);
148        assert!(md.starts_with("**type** `String`"));
149        assert!(md.contains("· stable"));
150        assert!(md.contains("· since"));
151        assert!(md.contains(s.body));
152    }
153
154    #[test]
155    fn iter_entries_yields_stable_order() {
156        let kinds: Vec<DocKind> = iter_entries().map(|e| e.kind).collect();
157        // Sorted by (kind, name) — types come before syntax (since
158        // DocKind::Type < DocKind::Syntax in the enum declaration
159        // order).
160        let mut sorted = kinds.clone();
161        sorted.sort();
162        assert_eq!(kinds, sorted, "iter_entries must yield stable order");
163    }
164}