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