Skip to content

Commit

Permalink
feat: Make Nushell try generating types
Browse files Browse the repository at this point in the history
  • Loading branch information
ysthakur committed Dec 30, 2023
1 parent e486366 commit 898537b
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 47 deletions.
4 changes: 3 additions & 1 deletion src/gen/bash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ pub fn generate(cmd: &CommandInfo) -> (String, String) {

out.writeln("return 0");
out.dedent();
out.writeln("}\n");
out.writeln("}");
out.writeln("");

out.writeln(format!("complete -F _comp_cmd_{} {}", cmd.name, cmd.name));
out.writeln("");

(format!("_{}.bash", cmd.name), out.text())
}
Expand Down
1 change: 1 addition & 0 deletions src/gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ fn preprocess(cmd: &CommandInfo) -> CommandInfo {
.collect();
CommandInfo {
name: cmd.name.clone(),
desc: cmd.desc.clone(),
flags,
args: cmd.args.clone(),
subcommands: cmd.subcommands.iter().map(preprocess).collect(),
Expand Down
131 changes: 102 additions & 29 deletions src/gen/nu.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
use crate::gen::{util::Output, CommandInfo};
use crate::{
gen::{util::Output, CommandInfo},
ArgType,
};

/// Generate completions for Nushell
pub fn generate(cmd: &CommandInfo) -> (String, String) {
let mut res = Output::new(String::from(" "));
generate_cmd(&cmd.name, cmd, &mut res, true);
generate_cmd(&cmd.name, cmd, &mut res);
(format!("{}-completions.nu", cmd.name), res.text())
}

fn generate_cmd(
cmd_name: &str,
cmd: &CommandInfo,
out: &mut Output,
first: bool,
) {
if !first {
// Avoid an extra line at the beginning of the file
out.writeln("");
}
out.writeln(format!("export extern \"{cmd_name}\" ["));
out.indent();

fn generate_cmd(cmd_name: &str, cmd: &CommandInfo, out: &mut Output) {
// Instead of immediately writing the flags to the command, build up a list of
// formatted flags here. If we need to, generate nu-complete commands to
// complete flags first, then the actual export extern, so that the extern's
// signature can use the `nu-complete` command for completing flags
let mut flags_strs = vec![];
// Flags that will need a nu-complete function to complete them
let mut complicated_flags = Vec::new();
for flag in &cmd.flags {
let (short, long): (Vec<_>, Vec<_>) =
let (short_forms, long_forms): (Vec<_>, Vec<_>) =
flag.forms.iter().partition(|f| f.len() == 2);

let desc_str = if let Some(desc) = &flag.desc {
Expand All @@ -30,29 +28,104 @@ fn generate_cmd(
String::new()
};

let type_str = if let Some(typ) = flag.typ.as_ref() {
match typ {
ArgType::Unknown => ": string".to_owned(),
_ => {
// Turn it into a valid Nu identifier
let first_form = if flag.forms[0].starts_with("--") {
&flag.forms[0][2..]
} else if flag.forms[0].starts_with('-') {
&flag.forms[0][1..]
} else {
&flag.forms[0]
};
// This may cause collisions if there are flags with underscores, but
// that's unlikely
let first_form = first_form.replace("-", "_");
let res =
format!(r#": string@"nu-complete {} {}""#, cmd_name, &first_form);
complicated_flags.push((first_form, typ));
res
}
}
} else {
String::new()
};

// Pair off as many long and short forms as possible
// It's unlikely there'll be both long and short forms of the same flag, but
// you never know what kind of horrors a man page may hold
let mut short = short.into_iter();
let mut long = long.into_iter();
while short.len() > 0 && long.len() > 0 {
let short_str = format!("({})", short.next().unwrap());
out.writeln(format!("{}{}{}", long.next().unwrap(), short_str, desc_str));
// It's unlikely there'll be multiple long *and* short forms of the same
// flag, but you never know what kind of horrors a man page may hold
let mut short_forms = short_forms.into_iter();
let mut long_forms = long_forms.into_iter();
while short_forms.len() > 0 && long_forms.len() > 0 {
flags_strs.push(format!(
"{}({}){}{}",
long_forms.next().unwrap(),
short_forms.next().unwrap(),
type_str,
desc_str
));
}

for flag in long {
out.writeln(format!("{flag}{desc_str}"));
for form in long_forms.into_iter().chain(short_forms) {
flags_strs.push(format!("{form}{type_str}{desc_str}"));
}
}

for flag in short {
out.writeln(format!("{flag}{desc_str}"));
}
// Generate functions to complete the more complicated flags. The flag to
// complete is the last part of the command name rather than an argument.

for (flag, typ) in complicated_flags {
out.writeln(format!(r#"def "nu-complete {} {}" [] {{"#, cmd_name, flag));
out.indent();
out.writeln(complete_type(typ));
out.dedent();
out.writeln("}");
out.writeln("");
}

// Generate the actual `export extern` command
if let Some(desc) = cmd.desc.as_ref() {
for line in desc.lines() {
out.writeln(format!("# {}", line));
}
}
out.writeln(format!("export extern \"{cmd_name}\" ["));
out.indent();
out.writeln(flags_strs.join("\n"));
out.dedent();
out.writeln("]");
out.writeln("");

for sub_cmd in &cmd.subcommands {
generate_cmd(&format!("{cmd_name} {}", sub_cmd.name), sub_cmd, out, false);
generate_cmd(&format!("{cmd_name} {}", sub_cmd.name), sub_cmd, out);
}
}

/// Generate Nu code to provide completions for a particular type
fn complete_type(typ: &ArgType) -> String {
match typ {
ArgType::Unknown => complete_type(&ArgType::Path),
ArgType::Run(cmd) => format!("({})", cmd),
ArgType::Strings(strs) => {
format!(
"[{}]",
strs
.iter()
.map(|s| format!("'{}'", s))
.collect::<Vec<_>>()
.join(", ")
)
}
ArgType::Any(types) => format!(
"[{}]",
types
.iter()
.map(|typ| format!("...{}", complete_type(typ)))
.collect::<Vec<_>>()
.join(" ")
),
_ => "[]".to_owned(), // todo implement
}
}
27 changes: 22 additions & 5 deletions src/gen/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,37 @@ impl Output {
self.indent_level -= 1;
}

fn write_indent(&mut self) {
for _ in 0..self.indent_level {
self.text.push_str(&self.indent_str);
}
}

/// Write some text (without a newline)
pub fn write<S: AsRef<str>>(&mut self, s: S) {
if self.line_ended {
for _ in 0..self.indent_level {
self.text.push_str(&self.indent_str);
}
self.write_indent();
self.line_ended = false;
}
self.text.push_str(s.as_ref());

let mut lines = s.as_ref().split('\n');
if let Some(mut line) = lines.next() {
loop {
self.text.push_str(line);
if let Some(next) = lines.next() {
self.text.push('\n');
self.write_indent();
line = next;
} else {
break;
}
}
}
}

/// Write some text (with a newline)
pub fn writeln<S: AsRef<str>>(&mut self, s: S) {
self.write(s);
self.write(s.as_ref());
self.text.push('\n');
self.line_ended = true;
}
Expand Down
2 changes: 1 addition & 1 deletion src/gen/zsh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ fn generate_fn(cmd: &CommandInfo, out: &mut Output, fn_name: &str) {

if cmd.subcommands.is_empty() {
out.dedent();
out.write("\n");
out.writeln("");
} else {
let sub_cmds = cmd
.subcommands
Expand Down
7 changes: 6 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Eq, Serialize, PartialEq)]
pub struct CommandInfo {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub desc: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub flags: Vec<Flag>,
/// The types of the arguments to this command
Expand Down Expand Up @@ -39,11 +41,14 @@ pub enum ArgType {
Dir,

/// Complete by running a command
Command(String),
Run(String),

/// Only these strings are allowed
Strings(Vec<String>),

/// Complete with the name of a command
CommandName,

/// Any of the given types work
Any(Vec<ArgType>),

Expand Down
Loading

0 comments on commit 898537b

Please sign in to comment.