Skip to main content

kaish_help/
topic.rs

1//! Topic compatibility surface for kaish help.
2//!
3//! Backs the `help <topic>` builtin and embedder prompt surfaces: topic-based
4//! whole-document help embedded at compile time, plus dynamic tool help from the
5//! tool registry.
6//! Behavior here is intentionally byte-stable — frontends and tests depend on it.
7
8use kaish_types::ToolSchema;
9
10use crate::content::{IGNORE, LIMITS, OUTPUT_LIMIT, OVERLAY, OVERVIEW, SCATTER, SYNTAX, VFS};
11
12/// Help topics available in kaish.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum HelpTopic {
15    /// Overview of kaish with topic list.
16    Overview,
17    /// Syntax reference: variables, quoting, pipes, control flow.
18    Syntax,
19    /// List of all available builtins.
20    Builtins,
21    /// Virtual filesystem mounts and paths.
22    Vfs,
23    /// Scatter/gather parallel processing.
24    Scatter,
25    /// Ignore file configuration.
26    Ignore,
27    /// Output size limit configuration.
28    OutputLimit,
29    /// Known limitations.
30    Limits,
31    /// Overlay VFS mode and kaish-vfs builtin.
32    Overlay,
33    /// Help for a specific tool.
34    Tool(String),
35}
36
37impl HelpTopic {
38    /// Parse a topic string into a HelpTopic.
39    ///
40    /// Returns Overview for empty/None, specific topics for known names,
41    /// or Tool(name) for anything else (assumes it's a tool name).
42    pub fn parse_topic(s: &str) -> Self {
43        match s.to_lowercase().as_str() {
44            "" | "overview" | "help" => Self::Overview,
45            "syntax" | "language" | "lang" => Self::Syntax,
46            "builtins" | "tools" | "commands" => Self::Builtins,
47            "vfs" | "filesystem" | "fs" | "paths" => Self::Vfs,
48            "scatter" | "gather" | "parallel" | "散" | "集" => Self::Scatter,
49            "ignore" | "gitignore" | "kaish-ignore" => Self::Ignore,
50            "output-limit" | "spill" | "truncate" | "kaish-output-limit" => Self::OutputLimit,
51            "limits" | "limitations" | "missing" => Self::Limits,
52            "overlay" | "kaish-vfs" | "vfs-overlay" => Self::Overlay,
53            other => Self::Tool(other.to_string()),
54        }
55    }
56
57    /// Get a short description of this topic.
58    pub fn description(&self) -> &'static str {
59        match self {
60            Self::Overview => "What kaish is, list of topics",
61            Self::Syntax => "Variables, quoting, pipes, control flow",
62            Self::Builtins => "List of available builtins",
63            Self::Vfs => "Virtual filesystem mounts and paths",
64            Self::Scatter => "Parallel processing (散/集)",
65            Self::Ignore => "Ignore file configuration",
66            Self::OutputLimit => "Output size limit configuration",
67            Self::Limits => "Known limitations",
68            Self::Overlay => "Copy-on-write overlay mode and kaish-vfs",
69            Self::Tool(_) => "Help for a specific tool",
70        }
71    }
72}
73
74/// Get help content for a topic.
75///
76/// For static topics, returns embedded markdown.
77/// For `Builtins`, generates a tool list from the provided schemas.
78/// For `Tool(name)`, looks up the tool in the schemas.
79pub fn get_help(topic: &HelpTopic, tool_schemas: &[ToolSchema]) -> String {
80    match topic {
81        HelpTopic::Overview => OVERVIEW.to_string(),
82        HelpTopic::Syntax => SYNTAX.to_string(),
83        HelpTopic::Builtins => format_tool_list(tool_schemas),
84        HelpTopic::Vfs => VFS.to_string(),
85        HelpTopic::Scatter => SCATTER.to_string(),
86        HelpTopic::Ignore => IGNORE.to_string(),
87        HelpTopic::OutputLimit => OUTPUT_LIMIT.to_string(),
88        HelpTopic::Limits => LIMITS.to_string(),
89        HelpTopic::Overlay => OVERLAY.to_string(),
90        HelpTopic::Tool(name) => format_tool_help(name, tool_schemas),
91    }
92}
93
94/// Format help for a single tool, or `None` if no such tool is registered.
95///
96/// The composition surface uses this; the `Unknown topic…` fallback lives in
97/// [`format_tool_help`] for the `help <topic>` command path.
98pub fn tool_help(name: &str, schemas: &[ToolSchema]) -> Option<String> {
99    let schema = schemas.iter().find(|s| s.name == name)?;
100    let mut output = String::new();
101
102    output.push_str(&format!("{} — {}\n\n", schema.name, schema.description));
103
104    if schema.params.is_empty() {
105        output.push_str("No parameters.\n");
106    } else {
107        output.push_str("Parameters:\n");
108        for param in &schema.params {
109            let req = if param.required { " (required)" } else { "" };
110            output.push_str(&format!(
111                "  {} : {}{}\n    {}\n",
112                param.name, param.param_type, req, param.description
113            ));
114        }
115    }
116
117    if !schema.examples.is_empty() {
118        output.push_str("\nExamples:\n");
119        for example in &schema.examples {
120            output.push_str(&format!("  # {}\n", example.description));
121            output.push_str(&format!("  {}\n\n", example.code));
122        }
123    }
124
125    Some(output)
126}
127
128/// Format help for a single tool.
129fn format_tool_help(name: &str, schemas: &[ToolSchema]) -> String {
130    tool_help(name, schemas).unwrap_or_else(|| {
131        format!(
132            "Unknown topic or tool: {}\n\nUse 'help' to see available topics, or 'help builtins' for tool list.",
133            name
134        )
135    })
136}
137
138/// Format a flat alphabetical list of all available tools.
139///
140/// Schemas arrive sorted from the registry; only registered tools appear,
141/// so feature-gated or unloaded builtins are omitted naturally.
142fn format_tool_list(schemas: &[ToolSchema]) -> String {
143    let mut output = String::from("# Available Builtins\n\n");
144
145    let max_len = schemas.iter().map(|s| s.name.len()).max().unwrap_or(0);
146
147    for schema in schemas {
148        output.push_str(&format!(
149            "  {:width$}  {}\n",
150            schema.name,
151            schema.description,
152            width = max_len
153        ));
154    }
155
156    output.push_str("\n---\n");
157    output.push_str("Use 'help <tool>' for detailed help on a specific tool.\n");
158    output.push_str("Use 'help syntax' for language syntax reference.\n");
159
160    output
161}
162
163/// List available help topics (for autocomplete, etc.).
164pub fn list_topics() -> Vec<(&'static str, &'static str)> {
165    vec![
166        ("overview", "What kaish is, list of topics"),
167        ("syntax", "Variables, quoting, pipes, control flow"),
168        ("builtins", "List of available builtins"),
169        ("vfs", "Virtual filesystem mounts and paths"),
170        ("scatter", "Parallel processing (散/集)"),
171        ("ignore", "Ignore file configuration"),
172        ("output-limit", "Output size limit configuration"),
173        ("limits", "Known limitations"),
174        ("overlay", "Copy-on-write overlay mode and kaish-vfs"),
175    ]
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_topic_parsing() {
184        assert_eq!(HelpTopic::parse_topic(""), HelpTopic::Overview);
185        assert_eq!(HelpTopic::parse_topic("overview"), HelpTopic::Overview);
186        assert_eq!(HelpTopic::parse_topic("syntax"), HelpTopic::Syntax);
187        assert_eq!(HelpTopic::parse_topic("SYNTAX"), HelpTopic::Syntax);
188        assert_eq!(HelpTopic::parse_topic("builtins"), HelpTopic::Builtins);
189        assert_eq!(HelpTopic::parse_topic("vfs"), HelpTopic::Vfs);
190        assert_eq!(HelpTopic::parse_topic("scatter"), HelpTopic::Scatter);
191        assert_eq!(HelpTopic::parse_topic("集"), HelpTopic::Scatter);
192        assert_eq!(HelpTopic::parse_topic("output-limit"), HelpTopic::OutputLimit);
193        assert_eq!(HelpTopic::parse_topic("spill"), HelpTopic::OutputLimit);
194        assert_eq!(HelpTopic::parse_topic("kaish-output-limit"), HelpTopic::OutputLimit);
195        assert_eq!(HelpTopic::parse_topic("limits"), HelpTopic::Limits);
196        assert_eq!(
197            HelpTopic::parse_topic("grep"),
198            HelpTopic::Tool("grep".to_string())
199        );
200    }
201
202    #[test]
203    fn test_static_content_embedded() {
204        // Verify the markdown files are embedded
205        assert!(OVERVIEW.contains("kaish"));
206        assert!(SYNTAX.contains("Variables"));
207        assert!(VFS.contains("Mount Points"));
208        assert!(SCATTER.contains("scatter"));
209        assert!(IGNORE.contains("kaish-ignore"));
210        assert!(OUTPUT_LIMIT.contains("kaish-output-limit"));
211        assert!(LIMITS.contains("Limitations"));
212    }
213
214    #[test]
215    fn test_get_help_overview() {
216        let content = get_help(&HelpTopic::Overview, &[]);
217        assert!(content.contains("kaish"));
218        assert!(content.contains("help syntax"));
219    }
220
221    #[test]
222    fn test_get_help_unknown_tool() {
223        let content = get_help(&HelpTopic::Tool("nonexistent".to_string()), &[]);
224        assert!(content.contains("Unknown topic or tool"));
225    }
226
227    #[test]
228    fn test_tool_help_none_for_missing() {
229        assert!(tool_help("nonexistent", &[]).is_none());
230    }
231}