Skip to content

Commit

Permalink
initial implementation of completion for fish
Browse files Browse the repository at this point in the history
  • Loading branch information
tertsdiepraam committed Dec 7, 2023
1 parent 0d975e3 commit 80979f0
Show file tree
Hide file tree
Showing 11 changed files with 250 additions and 22 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- uses: actions-rs/cargo@v1
with:
command: test
args: --all-features --workspace
args: --features complete --workspace

rustfmt:
name: Rustfmt
Expand Down
9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ readme = "README.md"

[dependencies]
uutils-args-derive = { version = "0.1.0", path = "derive" }
uutils-args-complete = { version = "0.1.0", path = "complete", optional = true }
strsim = "0.10.0"
lexopt = "0.3.0"

[features]
parse-is-complete = ["complete"]
complete = ["uutils-args-complete"]

[workspace]
members = [
"derive",
]
members = ["derive", "complete"]
10 changes: 10 additions & 0 deletions complete/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "uutils-args-complete"
version = "0.1.0"
edition = "2021"
authors = ["Terts Diepraam"]
license = "MIT"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
1 change: 1 addition & 0 deletions complete/LICENSE
67 changes: 67 additions & 0 deletions complete/src/fish.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

use crate::{Command, ValueHint};

pub fn render(c: &Command) -> String {
let mut out = String::new();
let name = &c.name;
for arg in &c.args {
let mut line = format!("complete -c {name}");
for short in &arg.short {
line.push_str(&format!(" -s {short}"));
}
for long in &arg.long {
line.push_str(&format!(" -l {long}"));
}
line.push_str(&format!(" -d '{}'", arg.help));
if let Some(value) = &arg.value {
line.push_str(&render_value_hint(value));
}
out.push_str(&line);
out.push('\n');
}
out
}

fn render_value_hint(value: &ValueHint) -> String {
match value {
ValueHint::Strings(s) => {
let joined = s.join(", ");
format!(" -a {{ {joined} }}")
}
_ => todo!(),
}
}

#[cfg(test)]
mod test {
use super::render;
use crate::{Arg, Command};

#[test]
fn short() {
let c = Command {
name: "test".into(),
args: vec![Arg {
short: vec!["a".into()],
help: "some flag".into(),
..Arg::default()
}],
};
assert_eq!(render(&c), "complete -c test -s a -d 'some flag'\n",)
}

#[test]
fn long() {
let c = Command {
name: "test".into(),
args: vec![Arg {
long: vec!["all".into()],
help: "some flag".into(),
..Arg::default()
}],
};
assert_eq!(render(&c), "complete -c test -l all -d 'some flag'\n",)
}
}
40 changes: 40 additions & 0 deletions complete/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

mod fish;

pub struct Command {
pub name: String,
pub args: Vec<Arg>,
}

#[derive(Default)]
pub struct Arg {
pub short: Vec<String>,
pub long: Vec<String>,
pub help: String,
pub value: Option<ValueHint>,
}

pub enum ValueHint {
Strings(Vec<String>),
Unknown,
// Other,
AnyPath,
// FilePath,
// DirPath,
// ExecutablePath,
// CommandName,
// CommandString,
// CommandWithArguments,
// Username,
// Hostname,
}

pub fn render(c: &Command, shell: &str) -> String {
match shell {
"fish" => fish::render(c),
"sh" | "zsh" | "bash" | "csh" => panic!("shell '{shell}' completion is not supported yet!"),
_ => panic!("unknown shell '{shell}'!"),
}
}
45 changes: 45 additions & 0 deletions derive/src/complete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

use crate::{
argument::{ArgType, Argument},
flags::{Flags, Flag},
};
use proc_macro2::TokenStream;
use quote::quote;

pub fn complete(args: &[Argument]) -> TokenStream {
let mut arg_specs = Vec::new();

for Argument { help, arg_type, .. } in args {
let ArgType::Option {
flags,
hidden: false,
..
} = arg_type
else {
continue;
};

let Flags { short, long, .. } = flags;
if short.is_empty() && long.is_empty() {
continue;
}
let short: Vec<_> = short.iter().map(|Flag { flag, .. }| quote!(String::from(#flag))).collect();
let long: Vec<_> = long.iter().map(|Flag { flag, .. }| quote!(String::from(#flag))).collect();

arg_specs.push(quote!(
Arg {
short: vec![#(#short),*],
long: vec![#(#long),*],
help: String::from(#help),
value: None,
}
))
}

quote!(Command {
name: String::from(option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME"))),
args: vec![#(#arg_specs),*]
})
}
9 changes: 8 additions & 1 deletion derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod flags;
mod help;
mod help_parser;
mod initial;
mod complete;

use argument::{
free_handling, long_handling, parse_argument, parse_arguments_attr, positional_handling,
Expand Down Expand Up @@ -50,6 +51,7 @@ pub fn arguments(input: TokenStream) -> TokenStream {
&arguments_attr.version_flags,
&arguments_attr.file,
);
let completion = complete::complete(&arguments);
let help = help_handling(&arguments_attr.help_flags);
let version = version_handling(&arguments_attr.version_flags);
let version_string = quote!(format!(
Expand Down Expand Up @@ -80,7 +82,6 @@ pub fn arguments(input: TokenStream) -> TokenStream {
) -> Result<Option<uutils_args::Argument<Self>>, uutils_args::Error> {
use uutils_args::{Value, lexopt, Error, Argument};

// #number_argment
#free

let arg = match { #next_arg } {
Expand Down Expand Up @@ -110,6 +111,12 @@ pub fn arguments(input: TokenStream) -> TokenStream {
fn version() -> String {
#version_string
}

#[cfg(feature = "complete")]
fn complete() -> ::uutils_args_complete::Command {
use ::uutils_args_complete::{Command, Arg, ValueHint};
#completion
}
}
);

Expand Down
8 changes: 2 additions & 6 deletions examples/hello_world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@ use uutils_args::{Arguments, Initial, Options};
#[derive(Arguments)]
#[arguments(file = "examples/hello_world_help.md")]
enum Arg {
/// The *name* to **greet**
///
/// Just to show off, I can do multiple paragraphs and wrap text!
///
/// # Also headings!
/// The name to greet
#[arg("-n NAME", "--name=NAME", "name=NAME")]
Name(String),

/// The **number of times** to `greet`
/// The number of times to greet
#[arg("-c N", "--count=N")]
Count(u8),

Expand Down
64 changes: 53 additions & 11 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ pub use value::{Value, ValueError, ValueResult};

use std::{
ffi::{OsStr, OsString},
marker::PhantomData,
io::Write,
marker::PhantomData,
};

/// A wrapper around a type implementing [`Arguments`] that adds `Help`
Expand Down Expand Up @@ -108,6 +108,9 @@ pub trait Arguments: Sized {
while iter.next_arg()?.is_some() {}
Ok(())
}

#[cfg(feature = "complete")]
fn complete() -> uutils_args_complete::Command;
}

/// An iterator over arguments.
Expand Down Expand Up @@ -207,13 +210,31 @@ pub trait Options<Arg: Arguments>: Sized + Initial {
I: IntoIterator + 'static,
I::Item: Into<OsString>,
{
let mut _self = Self::initial();
let mut iter = Arg::parse(args);
while let Some(arg) = iter.next_arg()? {
_self.apply(arg);
// Hacky but it works: if the parse-is-complete flag
// is active the parse function becomes the complete
// function so that no additional functionality is
// necessary for users to generate completions.
#[cfg(feature = "parse-is-complete")]
{
print_completion::<_, Self, Arg>(args.into_iter());
std::process::exit(0);
}

#[cfg(not(feature = "parse-is-complete"))]
{
let mut _self = Self::initial();
let mut iter = Arg::parse(args);
while let Some(arg) = iter.next_arg()? {
_self.apply(arg);
}
Arg::check_missing(iter.positional_idx)?;
Ok(_self)
}
Arg::check_missing(iter.positional_idx)?;
Ok(_self)
}

#[cfg(feature = "complete")]
fn complete(shell: &str) -> String {
uutils_args_complete::render(&Arg::complete(), shell)
}
}

Expand All @@ -240,6 +261,22 @@ pub fn __echo_style_positional(p: &mut lexopt::Parser, short_args: &[char]) -> O
}
}

#[cfg(feature = "complete")]
fn print_completion<I, O: Options<Arg>, Arg: Arguments>(mut args: I)
where
I: Iterator + 'static,
I::Item: Into<OsString>,
{
let _exec_name = args.next();
let shell = args
.next()
.expect("Need a shell argument for completion.")
.into();
let shell = shell.to_string_lossy();
assert!(args.next().is_none(), "completion only takes one argument");
println!("{}", O::complete(&shell));
}

fn is_echo_style_positional(s: &OsStr, short_args: &[char]) -> bool {
let s = match s.to_str() {
Some(x) => x,
Expand Down Expand Up @@ -317,7 +354,12 @@ pub fn filter_suggestions(input: &str, long_options: &[&str], prefix: &str) -> V
.collect()
}

pub fn print_flags(mut w: impl Write, indent_size: usize, width: usize, options: impl IntoIterator<Item = (&'static str, &'static str)>) -> std::io::Result<()> {
pub fn print_flags(
mut w: impl Write,
indent_size: usize,
width: usize,
options: impl IntoIterator<Item = (&'static str, &'static str)>,
) -> std::io::Result<()> {
let indent = " ".repeat(indent_size);
writeln!(w, "\nOptions:")?;
for (flags, help_string) in options {
Expand All @@ -330,15 +372,15 @@ pub fn print_flags(mut w: impl Write, indent_size: usize, width: usize, options:
None => {
writeln!(w)?;
continue;
},
}
};
let help_indent = " ".repeat(width-flags.len()+2);
let help_indent = " ".repeat(width - flags.len() + 2);
writeln!(w, "{}{}", help_indent, line)?;
} else {
writeln!(w)?;
}

let help_indent = " ".repeat(width+indent_size+2);
let help_indent = " ".repeat(width + indent_size + 2);
for line in help_lines {
writeln!(w, "{}{}", help_indent, line)?;
}
Expand Down
Loading

0 comments on commit 80979f0

Please sign in to comment.