flag_rs/
completion_format.rs1use crate::active_help::ActiveHelp;
7use crate::completion::CompletionResult;
8use crate::context::Context;
9
10#[derive(Debug, Clone, Copy)]
12pub enum CompletionFormat {
13 Simple,
15 Display,
17 Zsh,
19 Fish,
21 Bash,
23}
24
25impl CompletionFormat {
26 pub fn from_shell_type(shell_type: Option<&str>) -> Self {
28 match shell_type {
29 Some("zsh") => Self::Zsh,
30 Some("fish") => Self::Fish,
31 Some("bash") => Self::Bash,
32 Some("display") => Self::Display,
33 _ => Self::Simple,
34 }
35 }
36
37 pub fn format(self, result: &CompletionResult, ctx: Option<&Context>) -> Vec<String> {
39 let mut output = match self {
40 Self::Simple | Self::Bash => {
41 result.values.clone()
43 }
44 Self::Display => {
45 Self::format_display(result)
47 }
48 Self::Zsh => {
49 Self::format_zsh(result)
51 }
52 Self::Fish => {
53 Self::format_fish(result)
55 }
56 };
57
58 if let Some(ctx) = ctx {
60 let help_messages = Self::format_active_help(&result.active_help, ctx, self);
61 output.extend(help_messages);
62 }
63
64 output
65 }
66
67 fn format_display(result: &CompletionResult) -> Vec<String> {
69 use crate::color;
70
71 let has_descriptions = result.descriptions.iter().any(|d| !d.is_empty());
72 if !has_descriptions {
73 return result.values.clone();
74 }
75
76 let max_width = result.values.iter().map(String::len).max().unwrap_or(0);
78 let column_width = max_width + 4;
79
80 result
81 .values
82 .iter()
83 .zip(&result.descriptions)
84 .map(|(value, desc)| {
85 if desc.is_empty() {
86 value.clone()
87 } else {
88 let padded = format!("{value:<column_width$}");
89 if color::should_colorize() {
90 format!("{padded}{}", color::dim(desc))
91 } else {
92 format!("{padded}{desc}")
93 }
94 }
95 })
96 .collect()
97 }
98
99 fn format_zsh(result: &CompletionResult) -> Vec<String> {
101 const MAX_WIDTH: usize = 80;
103
104 let max_value_width = result.values.iter().map(String::len).max().unwrap_or(0);
106 let padding = max_value_width.min(35) + 4;
109
110 result
111 .values
112 .iter()
113 .zip(&result.descriptions)
114 .map(|(value, desc)| {
115 let escaped_value = value.replace(':', "\\:");
117
118 if desc.is_empty() {
119 format!("{escaped_value}:{escaped_value} - ")
121 } else {
122 let formatted_desc = if value.len() <= 35 {
125 format!("{escaped_value:<padding$}- {desc}")
126 } else {
127 format!("{escaped_value} - {desc}")
129 };
130
131 let full_line = format!("{escaped_value}:{formatted_desc}");
133 if full_line.len() > MAX_WIDTH {
134 format!("{}...", &full_line[..MAX_WIDTH - 3])
135 } else {
136 full_line
137 }
138 }
139 })
140 .collect()
141 }
142
143 fn format_fish(result: &CompletionResult) -> Vec<String> {
145 const MAX_WIDTH: usize = 80;
147
148 let max_value_width = result.values.iter().map(String::len).max().unwrap_or(0);
150 let padding = max_value_width.min(35) + 4;
152
153 result
154 .values
155 .iter()
156 .zip(&result.descriptions)
157 .map(|(value, desc)| {
158 if desc.is_empty() {
159 value.clone()
161 } else {
162 let formatted_desc = if value.len() <= 35 {
165 format!("{value:<padding$}- {desc}")
166 } else {
167 format!("{value} - {desc}")
169 };
170
171 let full_line = format!("{value}\t{formatted_desc}");
173 if formatted_desc.len() > MAX_WIDTH {
174 let truncated_desc = format!("{}...", &formatted_desc[..MAX_WIDTH - 3]);
175 format!("{value}\t{truncated_desc}")
176 } else {
177 full_line
178 }
179 }
180 })
181 .collect()
182 }
183
184 fn format_active_help(
186 help_messages: &[ActiveHelp],
187 ctx: &Context,
188 format: Self,
189 ) -> Vec<String> {
190 let mut formatted = Vec::new();
191
192 for help in help_messages {
193 if help.should_display(ctx) {
194 match format {
195 Self::Bash => {
196 formatted.push(format!("_activehelp_ {}", help.message));
199 }
200 Self::Zsh => {
201 formatted.push(format!("_activehelp_::{}", help.message));
204 }
205 Self::Fish => {
206 formatted.push(format!("_activehelp_\t{}", help.message));
208 }
209 Self::Simple | Self::Display => {
210 formatted.push(format!("[HELP] {}", help.message));
212 }
213 }
214 }
215 }
216
217 formatted
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::completion::CompletionResult;
225
226 #[test]
227 fn test_zsh_format_with_empty_description() {
228 let result = CompletionResult::new()
229 .add("value-without-desc")
230 .add_with_description("value-with-desc", "This has a description");
231
232 let formatted = CompletionFormat::Zsh.format(&result, None);
233
234 assert_eq!(formatted.len(), 2);
236 assert!(formatted[0].starts_with("value-without-desc:"));
237 assert!(formatted[0].contains(" - "));
238 assert!(formatted[1].starts_with("value-with-desc:"));
239 }
240
241 #[test]
242 fn test_zsh_format_uuid_without_description() {
243 let result = CompletionResult::new().add("28cbc1d1-7750-4253-9f55-ae21b9156b9d");
245
246 let formatted = CompletionFormat::Zsh.format(&result, None);
247
248 assert_eq!(formatted.len(), 1);
249 assert!(formatted[0].contains(':'));
251 assert!(formatted[0].contains(" - "));
252 assert_eq!(
254 formatted[0],
255 "28cbc1d1-7750-4253-9f55-ae21b9156b9d:28cbc1d1-7750-4253-9f55-ae21b9156b9d - "
256 );
257 }
258
259 #[test]
260 fn test_empty_value_handling() {
261 let result = CompletionResult::new()
262 .add("")
263 .add_with_description("", "Empty value with description");
264
265 let formatted = CompletionFormat::Zsh.format(&result, None);
266
267 assert_eq!(formatted.len(), 2);
269 for line in &formatted {
270 assert!(line.contains(':'));
271 }
272 }
273
274 #[test]
275 fn test_special_characters_in_value() {
276 let result = CompletionResult::new()
277 .add("value:with:colons")
278 .add("value'with'quotes")
279 .add("value with spaces");
280
281 let formatted = CompletionFormat::Zsh.format(&result, None);
282
283 assert!(formatted[0].starts_with("value\\:with\\:colons:"));
285 assert_eq!(formatted.len(), 3);
287 for line in &formatted {
288 assert!(line.contains(" - "));
289 }
290 }
291
292 #[test]
293 fn test_fish_format_empty_description() {
294 let result = CompletionResult::new()
295 .add("no-desc-value")
296 .add_with_description("with-desc", "Description");
297
298 let formatted = CompletionFormat::Fish.format(&result, None);
299
300 assert_eq!(formatted[0], "no-desc-value");
302 assert!(formatted[1].contains('\t'));
303 }
304
305 #[test]
306 fn test_bash_format() {
307 let result = CompletionResult::new()
308 .add("value1")
309 .add_with_description("value2", "Description ignored for bash");
310
311 let formatted = CompletionFormat::Bash.format(&result, None);
312
313 assert_eq!(formatted, vec!["value1", "value2"]);
315 }
316
317 #[test]
318 fn test_line_length_limits() {
319 let long_value = "a".repeat(50);
320 let long_desc = "b".repeat(50);
321
322 let result = CompletionResult::new().add_with_description(&long_value, &long_desc);
323
324 let formatted = CompletionFormat::Zsh.format(&result, None);
325
326 for line in formatted {
328 assert!(line.len() <= 80, "Line too long: {} chars", line.len());
329 if line.len() == 80 {
330 assert!(
331 line.ends_with("..."),
332 "Long lines should be truncated with ..."
333 );
334 }
335 }
336 }
337
338 #[test]
339 fn test_active_help_formatting() {
340 let result = CompletionResult::new()
341 .add("option1")
342 .add_help_text("This is a help message")
343 .add_conditional_help("Conditional help", |_| true)
344 .add_conditional_help("Hidden help", |_| false);
345
346 let ctx = Context::new(vec![]);
347
348 let bash_formatted = CompletionFormat::Bash.format(&result, Some(&ctx));
350 assert!(bash_formatted.contains(&"option1".to_string()));
351 assert!(bash_formatted.contains(&"_activehelp_ This is a help message".to_string()));
352 assert!(bash_formatted.contains(&"_activehelp_ Conditional help".to_string()));
353 assert!(!bash_formatted.iter().any(|s| s.contains("Hidden help")));
354
355 let zsh_formatted = CompletionFormat::Zsh.format(&result, Some(&ctx));
357 assert!(
358 zsh_formatted
359 .iter()
360 .any(|s| s.contains("_activehelp_::This is a help message"))
361 );
362 assert!(
363 zsh_formatted
364 .iter()
365 .any(|s| s.contains("_activehelp_::Conditional help"))
366 );
367
368 let fish_formatted = CompletionFormat::Fish.format(&result, Some(&ctx));
370 assert!(fish_formatted.contains(&"option1".to_string()));
371 assert!(fish_formatted.contains(&"_activehelp_\tThis is a help message".to_string()));
372 assert!(fish_formatted.contains(&"_activehelp_\tConditional help".to_string()));
373
374 let no_ctx_formatted = CompletionFormat::Bash.format(&result, None);
376 assert!(!no_ctx_formatted.iter().any(|s| s.contains("_activehelp_")));
377 }
378}