Repository: kbknapp/clap-rs
Pull request #1302 (2018-06-24 03:11:49)

feat(Completions): adds completion support for Elvish.


<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/kbknapp/clap-rs/1302)
<!-- Reviewable:end -->



Patch:
diff --git a/src/app/parser.rs b/src/app/parser.rs
index f9c04227..0f1e0efe 100644
--- a/src/app/parser.rs
+++ b/src/app/parser.rs
@@ -124,6 +124,7 @@ where
             Shell::Fish => format!("{}.fish", name),
             Shell::Zsh => format!("_{}", name),
             Shell::PowerShell => format!("_{}.ps1", name),
+            Shell::Elvish => format!("{}.elv", name),
         };
 
         let mut file = match File::create(out_dir.join(file_name)) {
diff --git a/src/completions/elvish.rs b/src/completions/elvish.rs
new file mode 100644
index 00000000..9a5f21a3
--- /dev/null
+++ b/src/completions/elvish.rs
@@ -0,0 +1,126 @@
+// Std
+use std::io::Write;
+
+// Internal
+use app::parser::Parser;
+use INTERNAL_ERROR_MSG;
+
+pub struct ElvishGen<'a, 'b>
+where
+    'a: 'b,
+{
+    p: &'b Parser<'a, 'b>,
+}
+
+impl<'a, 'b> ElvishGen<'a, 'b> {
+    pub fn new(p: &'b Parser<'a, 'b>) -> Self { ElvishGen { p: p } }
+
+    pub fn generate_to<W: Write>(&self, buf: &mut W) {
+        let bin_name = self.p.meta.bin_name.as_ref().unwrap();
+
+        let mut names = vec![];
+        let subcommands_cases =
+            generate_inner(self.p, "", &mut names);
+
+        let result = format!(r#"
+edit:completion:arg-completer[{bin_name}] = [@words]{{
+    fn spaces [n]{{
+        repeat $n ' ' | joins ''
+    }}
+    fn cand [text desc]{{
+        edit:complex-candidate $text &display-suffix=' '(spaces (- 14 (wcswidth $text)))$desc
+    }}
+    command = '{bin_name}'
+    for word $words[1:-1] {{
+        if (has-prefix $word '-') {{
+            break
+        }}
+        command = $command';'$word
+    }}
+    completions = [{subcommands_cases}
+    ]
+    $completions[$command]
+}}
+"#,
+            bin_name = bin_name,
+            subcommands_cases = subcommands_cases
+        );
+
+        w!(buf, result.as_bytes());
+    }
+}
+
+// Escape string inside single quotes
+fn escape_string(string: &str) -> String { string.replace("'", "''") }
+
+fn get_tooltip<T : ToString>(help: Option<&str>, data: T) -> String {
+    match help {
+        Some(help) => escape_string(help),
+        _ => data.to_string()
+    }
+}
+
+fn generate_inner<'a, 'b, 'p>(
+    p: &'p Parser<'a, 'b>,
+    previous_command_name: &str,
+    names: &mut Vec<&'p str>,
+) -> String {
+    debugln!("ElvishGen::generate_inner;");
+    let command_name = if previous_command_name.is_empty() {
+        p.meta.bin_name.as_ref().expect(INTERNAL_ERROR_MSG).clone()
+    } else {
+        format!("{};{}", previous_command_name, &p.meta.name)
+    };
+
+    let mut completions = String::new();
+    let preamble = String::from("\n            cand ");
+
+    for option in p.opts() {
+        if let Some(data) = option.s.short {
+            let tooltip = get_tooltip(option.b.help, data);
+            completions.push_str(&preamble);
+            completions.push_str(format!("-{} '{}'", data, tooltip).as_str());
+        }
+        if let Some(data) = option.s.long {
+            let tooltip = get_tooltip(option.b.help, data);
+            completions.push_str(&preamble);
+            completions.push_str(format!("--{} '{}'", data, tooltip).as_str());
+        }
+    }
+
+    for flag in p.flags() {
+        if let Some(data) = flag.s.short {
+            let tooltip = get_tooltip(flag.b.help, data);
+            completions.push_str(&preamble);
+            completions.push_str(format!("-{} '{}'", data, tooltip).as_str());
+        }
+        if let Some(data) = flag.s.long {
+            let tooltip = get_tooltip(flag.b.help, data);
+            completions.push_str(&preamble);
+            completions.push_str(format!("--{} '{}'", data, tooltip).as_str());
+        }
+    }
+
+    for subcommand in &p.subcommands {
+        let data = &subcommand.p.meta.name;
+        let tooltip = get_tooltip(subcommand.p.meta.about, data);
+        completions.push_str(&preamble);
+        completions.push_str(format!("{} '{}'", data, tooltip).as_str());
+    }
+
+    let mut subcommands_cases = format!(
+        r"
+        &'{}'= {{{}
+        }}",
+        &command_name,
+        completions
+    );
+
+    for subcommand in &p.subcommands {
+        let subcommand_subcommands_cases =
+            generate_inner(&subcommand.p, &command_name, names);
+        subcommands_cases.push_str(&subcommand_subcommands_cases);
+    }
+
+    subcommands_cases
+}
diff --git a/src/completions/mod.rs b/src/completions/mod.rs
index e8775989..a3306d72 100644
--- a/src/completions/mod.rs
+++ b/src/completions/mod.rs
@@ -4,6 +4,7 @@ mod bash;
 mod fish;
 mod zsh;
 mod powershell;
+mod elvish;
 mod shell;
 
 // Std
@@ -15,6 +16,7 @@ use self::bash::BashGen;
 use self::fish::FishGen;
 use self::zsh::ZshGen;
 use self::powershell::PowerShellGen;
+use self::elvish::ElvishGen;
 pub use self::shell::Shell;
 
 pub struct ComplGen<'a, 'b>
@@ -33,6 +35,7 @@ impl<'a, 'b> ComplGen<'a, 'b> {
             Shell::Fish => FishGen::new(self.p).generate_to(buf),
             Shell::Zsh => ZshGen::new(self.p).generate_to(buf),
             Shell::PowerShell => PowerShellGen::new(self.p).generate_to(buf),
+            Shell::Elvish => ElvishGen::new(self.p).generate_to(buf),
         }
     }
 }
diff --git a/src/completions/shell.rs b/src/completions/shell.rs
index ceec9ff5..19aab869 100644
--- a/src/completions/shell.rs
+++ b/src/completions/shell.rs
@@ -15,11 +15,13 @@ pub enum Shell {
     Zsh,
     /// Generates a completion file for PowerShell
     PowerShell,
+    /// Generates a completion file for Elvish
+    Elvish,
 }
 
 impl Shell {
     /// A list of possible variants in `&'static str` form
-    pub fn variants() -> [&'static str; 4] { ["zsh", "bash", "fish", "powershell"] }
+    pub fn variants() -> [&'static str; 5] { ["zsh", "bash", "fish", "powershell", "elvish"] }
 }
 
 impl FromStr for Shell {
@@ -31,7 +33,8 @@ impl FromStr for Shell {
             "FISH" | _ if s.eq_ignore_ascii_case("fish") => Ok(Shell::Fish),
             "BASH" | _ if s.eq_ignore_ascii_case("bash") => Ok(Shell::Bash),
             "POWERSHELL" | _ if s.eq_ignore_ascii_case("powershell") => Ok(Shell::PowerShell),
-            _ => Err(String::from("[valid values: bash, fish, zsh, powershell]")),
+            "ELVISH" | _ if s.eq_ignore_ascii_case("elvish") => Ok(Shell::Elvish),
+            _ => Err(String::from("[valid values: bash, fish, zsh, powershell, elvish]")),
         }
     }
 }
@@ -43,6 +46,7 @@ impl fmt::Display for Shell {
             Shell::Fish => write!(f, "FISH"),
             Shell::Zsh => write!(f, "ZSH"),
             Shell::PowerShell => write!(f, "POWERSHELL"),
+            Shell::Elvish => write!(f, "ELVISH"),
         }
     }
 }


Test patch:
diff --git a/tests/completions.rs b/tests/completions.rs
index 48705ceb..34566d3e 100644
--- a/tests/completions.rs
+++ b/tests/completions.rs
@@ -228,6 +228,105 @@ Register-ArgumentCompleter -Native -CommandName 'my_app' -ScriptBlock {
 }
 "#;
 
+static ELVISH: &'static str = r#"
+edit:completion:arg-completer[my_app] = [@words]{
+    fn spaces [n]{
+        repeat $n ' ' | joins ''
+    }
+    fn cand [text desc]{
+        edit:complex-candidate $text &display-suffix=' '(spaces (- 14 (wcswidth $text)))$desc
+    }
+    command = 'my_app'
+    for word $words[1:-1] {
+        if (has-prefix $word '-') {
+            break
+        }
+        command = $command';'$word
+    }
+    completions = [
+        &'my_app'= {
+            cand -h 'Prints help information'
+            cand --help 'Prints help information'
+            cand -V 'Prints version information'
+            cand --version 'Prints version information'
+            cand test 'tests things'
+            cand help 'Prints this message or the help of the given subcommand(s)'
+        }
+        &'my_app;test'= {
+            cand --case 'the case to test'
+            cand -h 'Prints help information'
+            cand --help 'Prints help information'
+            cand -V 'Prints version information'
+            cand --version 'Prints version information'
+        }
+        &'my_app;help'= {
+            cand -h 'Prints help information'
+            cand --help 'Prints help information'
+            cand -V 'Prints version information'
+            cand --version 'Prints version information'
+        }
+    ]
+    $completions[$command]
+}
+"#;
+
+static ELVISH_SPECIAL_CMDS: &'static str = r#"
+edit:completion:arg-completer[my_app] = [@words]{
+    fn spaces [n]{
+        repeat $n ' ' | joins ''
+    }
+    fn cand [text desc]{
+        edit:complex-candidate $text &display-suffix=' '(spaces (- 14 (wcswidth $text)))$desc
+    }
+    command = 'my_app'
+    for word $words[1:-1] {
+        if (has-prefix $word '-') {
+            break
+        }
+        command = $command';'$word
+    }
+    completions = [
+        &'my_app'= {
+            cand -h 'Prints help information'
+            cand --help 'Prints help information'
+            cand -V 'Prints version information'
+            cand --version 'Prints version information'
+            cand test 'tests things'
+            cand some_cmd 'tests other things'
+            cand some-cmd-with-hypens 'some-cmd-with-hypens'
+            cand help 'Prints this message or the help of the given subcommand(s)'
+        }
+        &'my_app;test'= {
+            cand --case 'the case to test'
+            cand -h 'Prints help information'
+            cand --help 'Prints help information'
+            cand -V 'Prints version information'
+            cand --version 'Prints version information'
+        }
+        &'my_app;some_cmd'= {
+            cand --config 'the other case to test'
+            cand -h 'Prints help information'
+            cand --help 'Prints help information'
+            cand -V 'Prints version information'
+            cand --version 'Prints version information'
+        }
+        &'my_app;some-cmd-with-hypens'= {
+            cand -h 'Prints help information'
+            cand --help 'Prints help information'
+            cand -V 'Prints version information'
+            cand --version 'Prints version information'
+        }
+        &'my_app;help'= {
+            cand -h 'Prints help information'
+            cand --help 'Prints help information'
+            cand -V 'Prints version information'
+            cand --version 'Prints version information'
+        }
+    ]
+    $completions[$command]
+}
+"#;
+
 static POWERSHELL_SPECIAL_CMDS: &'static str = r#"
 using namespace System.Management.Automation
 using namespace System.Management.Automation.Language
@@ -703,6 +802,26 @@ fn powershell() {
     assert!(compare(&*string, POWERSHELL));
 }
 
+#[test]
+fn elvish() {
+    let mut app = build_app();
+    let mut buf = vec![];
+    app.gen_completions_to("my_app", Shell::Elvish, &mut buf);
+    let string = String::from_utf8(buf).unwrap();
+
+    assert!(compare(&*string, ELVISH));
+}
+
+#[test]
+fn elvish_with_special_commands() {
+    let mut app = build_app_special_commands();
+    let mut buf = vec![];
+    app.gen_completions_to("my_app", Shell::Elvish, &mut buf);
+    let string = String::from_utf8(buf).unwrap();
+
+    assert!(compare(&*string, ELVISH_SPECIAL_CMDS));
+}
+
 #[test]
 fn powershell_with_special_commands() {
     let mut app = build_app_special_commands();