Skip to main content

cell_sheet_core/help/
mod.rs

1pub mod entries;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum HelpCategory {
5    Normal,
6    Insert,
7    Visual,
8    Command,
9    Formula,
10}
11
12impl HelpCategory {
13    pub fn label(&self) -> &'static str {
14        match self {
15            HelpCategory::Normal => "NORMAL MODE",
16            HelpCategory::Insert => "INSERT MODE",
17            HelpCategory::Visual => "VISUAL MODE",
18            HelpCategory::Command => "COMMANDS",
19            HelpCategory::Formula => "FORMULAS",
20        }
21    }
22}
23
24#[derive(Debug)]
25pub struct HelpEntry {
26    pub tags: &'static [&'static str],
27    pub category: HelpCategory,
28    pub summary: &'static str,
29    pub detail: &'static str,
30}
31
32pub struct HelpRegistry {
33    entries: Vec<&'static HelpEntry>,
34}
35
36impl Default for HelpRegistry {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl HelpRegistry {
43    /// Build the default registry with all built-in help entries.
44    pub fn new() -> Self {
45        use entries::*;
46        Self::from_entries(&[
47            NORMAL_ENTRIES,
48            INSERT_ENTRIES,
49            VISUAL_ENTRIES,
50            COMMAND_ENTRIES,
51            FORMULA_ENTRIES,
52        ])
53    }
54
55    /// Build a registry from multiple static entry slices (one per module).
56    pub fn from_entries(slices: &[&'static [HelpEntry]]) -> Self {
57        let mut entries = Vec::new();
58        for slice in slices {
59            for entry in *slice {
60                entries.push(entry);
61            }
62        }
63        HelpRegistry { entries }
64    }
65
66    /// Find an entry by tag (case-insensitive).
67    pub fn find(&self, tag: &str) -> Option<&'static HelpEntry> {
68        let tag_lower = tag.to_lowercase();
69        for entry in &self.entries {
70            for t in entry.tags {
71                if t.to_lowercase() == tag_lower {
72                    return Some(entry);
73                }
74            }
75        }
76        None
77    }
78
79    /// Return all entries in a given category, in registration order.
80    pub fn by_category(&self, category: HelpCategory) -> Vec<&'static HelpEntry> {
81        self.entries
82            .iter()
83            .copied()
84            .filter(|e| e.category == category)
85            .collect()
86    }
87
88    /// Return all categories that have at least one entry, in display order.
89    pub fn categories(&self) -> Vec<HelpCategory> {
90        use HelpCategory::*;
91        let order = [Normal, Insert, Visual, Command, Formula];
92        order
93            .iter()
94            .copied()
95            .filter(|cat| self.entries.iter().any(|e| e.category == *cat))
96            .collect()
97    }
98
99    /// Return all entries in display order (grouped by category).
100    pub fn all_entries(&self) -> &[&'static HelpEntry] {
101        &self.entries
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    static TEST_ENTRIES: &[HelpEntry] = &[
110        HelpEntry {
111            tags: &["h"],
112            category: HelpCategory::Normal,
113            summary: "Move cursor left",
114            detail: "Move the cursor one column to the left.",
115        },
116        HelpEntry {
117            tags: &[":w", ":write"],
118            category: HelpCategory::Command,
119            summary: "Save file",
120            detail: "Write the current sheet to disk.",
121        },
122    ];
123
124    #[test]
125    fn find_by_tag() {
126        let registry = HelpRegistry::from_entries(&[TEST_ENTRIES]);
127        let entry = registry.find("h").unwrap();
128        assert_eq!(entry.summary, "Move cursor left");
129    }
130
131    #[test]
132    fn find_by_alias_tag() {
133        let registry = HelpRegistry::from_entries(&[TEST_ENTRIES]);
134        let entry = registry.find(":write").unwrap();
135        assert_eq!(entry.summary, "Save file");
136    }
137
138    #[test]
139    fn find_case_insensitive() {
140        let registry = HelpRegistry::from_entries(&[TEST_ENTRIES]);
141        let entry = registry.find("H").unwrap();
142        assert_eq!(entry.summary, "Move cursor left");
143    }
144
145    #[test]
146    fn find_not_found() {
147        let registry = HelpRegistry::from_entries(&[TEST_ENTRIES]);
148        assert!(registry.find("zzz").is_none());
149    }
150
151    #[test]
152    fn by_category() {
153        let registry = HelpRegistry::from_entries(&[TEST_ENTRIES]);
154        let normals = registry.by_category(HelpCategory::Normal);
155        assert_eq!(normals.len(), 1);
156        assert_eq!(normals[0].tags[0], "h");
157    }
158
159    #[test]
160    fn full_registry_has_expected_tags() {
161        let registry = HelpRegistry::new();
162        assert!(registry.find("h").is_some(), "missing h");
163        assert!(registry.find("dd").is_some(), "missing dd");
164        assert!(registry.find(":w").is_some(), "missing :w");
165        assert!(registry.find(":help").is_some(), "missing :help");
166        assert!(registry.find("SUM").is_some(), "missing SUM");
167        assert!(registry.find("IF").is_some(), "missing IF");
168        assert!(registry.find("Esc").is_some(), "missing Esc");
169        assert!(registry.find("v").is_some(), "missing v");
170    }
171}