1 use std::io::Write;
2 
3 use clap::builder::StyledStr;
4 use clap::Command;
5 
6 use crate::generator::{utils, Generator};
7 use crate::INTERNAL_ERROR_MSG;
8 
9 /// Generate elvish completion file
10 #[derive(Copy, Clone, PartialEq, Eq, Debug)]
11 pub struct Elvish;
12 
13 impl Generator for Elvish {
file_name(&self, name: &str) -> String14     fn file_name(&self, name: &str) -> String {
15         format!("{name}.elv")
16     }
17 
generate(&self, cmd: &Command, buf: &mut dyn Write)18     fn generate(&self, cmd: &Command, buf: &mut dyn Write) {
19         let bin_name = cmd
20             .get_bin_name()
21             .expect("crate::generate should have set the bin_name");
22 
23         let subcommands_cases = generate_inner(cmd, "");
24 
25         let result = format!(
26             r#"
27 use builtin;
28 use str;
29 
30 set edit:completion:arg-completer[{bin_name}] = {{|@words|
31     fn spaces {{|n|
32         builtin:repeat $n ' ' | str:join ''
33     }}
34     fn cand {{|text desc|
35         edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc
36     }}
37     var command = '{bin_name}'
38     for word $words[1..-1] {{
39         if (str:has-prefix $word '-') {{
40             break
41         }}
42         set command = $command';'$word
43     }}
44     var completions = [{subcommands_cases}
45     ]
46     $completions[$command]
47 }}
48 "#,
49         );
50 
51         w!(buf, result.as_bytes());
52     }
53 }
54 
55 // Escape string inside single quotes
escape_string(string: &str) -> String56 fn escape_string(string: &str) -> String {
57     string.replace('\'', "''")
58 }
59 
escape_help<T: ToString>(help: Option<&StyledStr>, data: T) -> String60 fn escape_help<T: ToString>(help: Option<&StyledStr>, data: T) -> String {
61     match help {
62         Some(help) => escape_string(&help.to_string().replace('\n', " ")),
63         _ => data.to_string(),
64     }
65 }
66 
generate_inner(p: &Command, previous_command_name: &str) -> String67 fn generate_inner(p: &Command, previous_command_name: &str) -> String {
68     debug!("generate_inner");
69 
70     let command_name = if previous_command_name.is_empty() {
71         p.get_bin_name().expect(INTERNAL_ERROR_MSG).to_string()
72     } else {
73         format!("{};{}", previous_command_name, &p.get_name())
74     };
75 
76     let mut completions = String::new();
77     let preamble = String::from("\n            cand ");
78 
79     for option in p.get_opts() {
80         if let Some(shorts) = option.get_short_and_visible_aliases() {
81             let tooltip = escape_help(option.get_help(), shorts[0]);
82             for short in shorts {
83                 completions.push_str(&preamble);
84                 completions.push_str(format!("-{short} '{tooltip}'").as_str());
85             }
86         }
87 
88         if let Some(longs) = option.get_long_and_visible_aliases() {
89             let tooltip = escape_help(option.get_help(), longs[0]);
90             for long in longs {
91                 completions.push_str(&preamble);
92                 completions.push_str(format!("--{long} '{tooltip}'").as_str());
93             }
94         }
95     }
96 
97     for flag in utils::flags(p) {
98         if let Some(shorts) = flag.get_short_and_visible_aliases() {
99             let tooltip = escape_help(flag.get_help(), shorts[0]);
100             for short in shorts {
101                 completions.push_str(&preamble);
102                 completions.push_str(format!("-{short} '{tooltip}'").as_str());
103             }
104         }
105 
106         if let Some(longs) = flag.get_long_and_visible_aliases() {
107             let tooltip = escape_help(flag.get_help(), longs[0]);
108             for long in longs {
109                 completions.push_str(&preamble);
110                 completions.push_str(format!("--{long} '{tooltip}'").as_str());
111             }
112         }
113     }
114 
115     for subcommand in p.get_subcommands() {
116         let data = &subcommand.get_name();
117         let tooltip = escape_help(subcommand.get_about(), data);
118 
119         completions.push_str(&preamble);
120         completions.push_str(format!("{data} '{tooltip}'").as_str());
121     }
122 
123     let mut subcommands_cases = format!(
124         r"
125         &'{}'= {{{}
126         }}",
127         &command_name, completions
128     );
129 
130     for subcommand in p.get_subcommands() {
131         let subcommand_subcommands_cases = generate_inner(subcommand, &command_name);
132         subcommands_cases.push_str(&subcommand_subcommands_cases);
133     }
134 
135     subcommands_cases
136 }
137