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 push_params(&mut output, &schema.params, " ");
120 }
121
122 if !schema.subcommands.is_empty() {
127 output.push_str("\nSubcommands:\n");
128 for sub in &schema.subcommands {
129 if sub.description.is_empty() {
130 output.push_str(&format!(" {}\n", sub.name));
131 } else {
132 output.push_str(&format!(" {} — {}\n", sub.name, sub.description));
133 }
134 push_params(&mut output, &sub.params, " ");
135 }
136 }
137
138 if !schema.examples.is_empty() {
139 output.push_str("\nExamples:\n");
140 for example in &schema.examples {
141 output.push_str(&format!(" # {}\n", example.description));
142 output.push_str(&format!(" {}\n\n", example.code));
143 }
144 }
145
146 Some(output)
147}
148
149fn push_params(output: &mut String, params: &[kaish_types::ParamSchema], indent: &str) {
155 for param in params {
156 let req = if param.required { " (required)" } else { "" };
157 let aliases = if param.aliases.is_empty() {
158 String::new()
159 } else {
160 format!(" (also: {})", param.aliases.join(", "))
161 };
162 output.push_str(&format!(
163 "{indent}{} : {}{}{}\n{indent} {}\n",
164 param.name, param.param_type, req, aliases, param.description
165 ));
166 }
167}
168
169fn format_tool_help(name: &str, schemas: &[ToolSchema]) -> String {
171 tool_help(name, schemas).unwrap_or_else(|| {
172 format!(
173 "Unknown topic or tool: {}\n\nUse 'help' to see available topics, or 'help builtins' for tool list.",
174 name
175 )
176 })
177}
178
179fn format_tool_list(schemas: &[ToolSchema]) -> String {
184 let mut output = String::from("# Available Builtins\n\n");
185
186 let max_len = schemas.iter().map(|s| s.name.len()).max().unwrap_or(0);
187
188 for schema in schemas {
189 output.push_str(&format!(
190 " {:width$} {}\n",
191 schema.name,
192 schema.description,
193 width = max_len
194 ));
195 }
196
197 output.push_str("\n---\n");
198 output.push_str("Use 'help <tool>' for detailed help on a specific tool.\n");
199 output.push_str("Use 'help syntax' for language syntax reference.\n");
200
201 output
202}
203
204pub fn list_topics() -> Vec<(&'static str, &'static str)> {
206 vec![
207 ("overview", "What kaish is, list of topics"),
208 ("syntax", "Variables, quoting, pipes, control flow"),
209 ("builtins", "List of available builtins"),
210 ("vfs", "Virtual filesystem mounts and paths"),
211 ("scatter", "Parallel processing (散/集)"),
212 ("ignore", "Ignore file configuration"),
213 ("output-limit", "Output size limit configuration"),
214 ("limits", "Known limitations"),
215 ("overlay", "Copy-on-write overlay mode and kaish-vfs"),
216 ("collections", "Lists & records: literals, access, iteration, lvalues"),
217 ]
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_topic_parsing() {
226 assert_eq!(HelpTopic::parse_topic(""), HelpTopic::Overview);
227 assert_eq!(HelpTopic::parse_topic("overview"), HelpTopic::Overview);
228 assert_eq!(HelpTopic::parse_topic("syntax"), HelpTopic::Syntax);
229 assert_eq!(HelpTopic::parse_topic("SYNTAX"), HelpTopic::Syntax);
230 assert_eq!(HelpTopic::parse_topic("builtins"), HelpTopic::Builtins);
231 assert_eq!(HelpTopic::parse_topic("vfs"), HelpTopic::Vfs);
232 assert_eq!(HelpTopic::parse_topic("scatter"), HelpTopic::Scatter);
233 assert_eq!(HelpTopic::parse_topic("集"), HelpTopic::Scatter);
234 assert_eq!(HelpTopic::parse_topic("output-limit"), HelpTopic::OutputLimit);
235 assert_eq!(HelpTopic::parse_topic("spill"), HelpTopic::OutputLimit);
236 assert_eq!(HelpTopic::parse_topic("kaish-output-limit"), HelpTopic::OutputLimit);
237 assert_eq!(HelpTopic::parse_topic("limits"), HelpTopic::Limits);
238 assert_eq!(
239 HelpTopic::parse_topic("grep"),
240 HelpTopic::Tool("grep".to_string())
241 );
242 assert_eq!(
243 HelpTopic::parse_topic("collections"),
244 HelpTopic::SyntaxSection("collections".to_string())
245 );
246 }
247
248 #[test]
249 fn test_get_help_collections_section() {
250 let content = get_help(&HelpTopic::SyntaxSection("collections".to_string()), &[]);
251 assert!(content.contains("Collections (lists & records)"));
252 assert!(content.contains("xs=[apple banana cherry]"));
253 assert!(SYNTAX.contains("xs=[apple banana cherry]"));
255 }
256
257 #[test]
258 fn test_get_help_unknown_syntax_section_falls_back() {
259 let content = get_help(&HelpTopic::SyntaxSection("not-a-real-section".to_string()), &[]);
262 assert!(content.contains("Unknown topic or tool"));
263 }
264
265 #[test]
266 fn test_static_content_embedded() {
267 assert!(OVERVIEW.contains("kaish"));
269 assert!(SYNTAX.contains("Variables"));
270 assert!(VFS.contains("Mount Points"));
271 assert!(SCATTER.contains("scatter"));
272 assert!(IGNORE.contains("kaish-ignore"));
273 assert!(OUTPUT_LIMIT.contains("kaish-output-limit"));
274 assert!(LIMITS.contains("Limitations"));
275 }
276
277 #[test]
278 fn test_get_help_overview() {
279 let content = get_help(&HelpTopic::Overview, &[]);
280 assert!(content.contains("kaish"));
281 assert!(content.contains("help syntax"));
282 }
283
284 #[test]
285 fn test_get_help_unknown_tool() {
286 let content = get_help(&HelpTopic::Tool("nonexistent".to_string()), &[]);
287 assert!(content.contains("Unknown topic or tool"));
288 }
289
290 #[test]
291 fn test_tool_help_none_for_missing() {
292 assert!(tool_help("nonexistent", &[]).is_none());
293 }
294}