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