1use kaish_types::ToolSchema;
9
10use crate::compose::render_syntax_section;
11use crate::content::{IGNORE, LIMITS, OUTPUT_LIMIT, OVERLAY, OVERVIEW, SCATTER, SYNTAX, VFS};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum HelpTopic {
16 Overview,
18 Syntax,
20 Builtins,
22 Vfs,
24 Scatter,
26 Ignore,
28 OutputLimit,
30 Limits,
32 Overlay,
34 SyntaxSection(String),
37 Tool(String),
39}
40
41impl HelpTopic {
42 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 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
80pub 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
105pub 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
139fn 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
149fn 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
174pub 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 assert!(SYNTAX.contains("xs=[apple banana cherry]"));
225 }
226
227 #[test]
228 fn test_get_help_unknown_syntax_section_falls_back() {
229 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 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}