Skip to content

Commit

Permalink
feat: WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
ModProg committed Sep 25, 2022
1 parent 3e87d0b commit 890c93e
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 6 deletions.
118 changes: 118 additions & 0 deletions clap_complete/src/dynamic/fish.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//! Complete commands within fish

// For fish the behaviour should match the default as close as possible
// 1. grouping shorthand completions i.e. -a<TAB> should show other shorthands to end up with
// -ams...
// 2. only complete options when one - is typed
// Due to https://github.com/fish-shell/fish-shell/issues/7943 we need to implement this our self
//

use std::{
ffi::OsString,
fmt::Display,
io::{stdout, Write},
};

use clap::Args;

use super::{complete, completions_for_arg, Completer, CompletionContext};

/// Dynamic completion for Fish
pub struct Fish;

impl Completer for Fish {
type CompleteArgs = CompleteArgs;

fn completion_script(cmd: &mut clap::Command) -> Vec<u8> {
let name = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name());
format!(
r#"complete -x -c {name} -a "{name} complete --previous (commandline --current-process --tokenize --cut-at-cursor) --current (commandline --current-token)""#
).into_bytes()
}

fn file_name(name: &str) -> String {
format!("{name}.fish")
}

fn try_complete(
CompleteArgs {
commandline,
current,
}: &Self::CompleteArgs,
cmd: &mut clap::Command,
) -> clap::error::Result<()> {
let CompletionContext {
value_for,
options,
subcommands,
} = complete(cmd, commandline)?;

let mut completions = Vec::new();
let current = current.to_string_lossy();

// fish only shows flags, when the user currently types a flag
if !options.is_empty() && current.starts_with("--") {
for option in options {
// TODO maybe only offer aliases when the user currently is typing one
// This could easily inflate the number of completions when a lot of aliases are
// used, on the other hand this can be achieved by using hidden aliases
if let Some(longs) = option.get_long_and_visible_aliases() {
for long in longs {
add_completion(&mut completions, long, option.get_help())?;
}
}
}
// TODO display non option values, in case the arg allows values starting with `--`
} else if !options.is_empty() && current.starts_with('-') {
// TODO implement flag joining i.e. show `-er` when the user typed `-e`
for option in options {
if let Some(shorts) = option.get_short_and_visible_aliases() {
for short in shorts {
add_completion(&mut completions, short, option.get_help())?;
}
}
}
// TODO display non option values, in case the arg allows values starting with `-`
} else if let Some(value_for) = value_for {
for (item, help) in completions_for_arg(value_for)? {
add_completion(&mut completions, item, help)?;
}
} else {
for subcommand in subcommands {
add_completion(
&mut completions,
subcommand.get_name(),
subcommand.get_about(),
)?;
for alias in subcommand.get_visible_aliases() {
add_completion(&mut completions, alias, subcommand.get_about())?;
}
}
}

stdout().write_all(&completions)?;
Ok(())
}
}

fn add_completion(
completions: &mut Vec<u8>,
item: impl Display,
help: Option<impl Display>,
) -> clap::error::Result<()> {
if let Some(help) = help {
writeln!(completions, "{item}\t{help}")?;
} else {
writeln!(completions, "{item}")?;
}
Ok(())
}

#[derive(Args, Clone, Debug)]
/// Arguments for Fish Completion
pub struct CompleteArgs {
/// commandline tokens before the cursor
commandline: Vec<OsString>,
/// token containing the cursor
current: OsString,
}
61 changes: 55 additions & 6 deletions clap_complete/src/dynamic/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
//! Complete commands within shells

pub mod bash;
pub mod fish;

use std::{io::Write, path::PathBuf};
use std::{
ffi::OsString,
io::{self, Write},
path::PathBuf,
};

use clap::{Args, Command, Subcommand, ValueEnum};
use clap::{Arg, Args, Command, Subcommand, ValueEnum};

use crate::dynamic::bash::Bash;
use bash::Bash;
use fish::Fish;

#[derive(Subcommand, Clone, Debug)]
#[command(hide = true)]
Expand All @@ -23,6 +29,7 @@ pub enum CompleteCommand {
/// Subcommand for all the shells, so each can have their own options
pub enum CompleteShell {
Bash(bash::CompleteArgs),
Fish(fish::CompleteArgs),
/// Only exception is Register, which outputs the completion script for a shell
Register(RegisterArgs),
}
Expand All @@ -44,19 +51,23 @@ pub struct RegisterArgs {
pub enum Shell {
/// Bourne Again SHell (bash)
Bash,
/// Friendly Interactive SHell (fish)
Fish,
}

impl Shell {
/// Return completion script
fn completion_script(&self, cmd: &mut Command) -> Vec<u8> {
match self {
Shell::Bash => Bash::completion_script(cmd),
Shell::Fish => Fish::completion_script(cmd),
}
}
/// The recommended file name for the registration code
fn file_name(&self, name: &str) -> String {
match self {
Shell::Bash => Bash::file_name(name),
Shell::Fish => Fish::file_name(name),
}
}
}
Expand All @@ -77,21 +88,22 @@ pub trait Completer {

impl CompleteCommand {
/// Process the completion request
pub fn complete(&self, cmd: &mut clap::Command) -> std::convert::Infallible {
pub fn complete(&self, cmd: &mut Command) -> std::convert::Infallible {
self.try_complete(cmd).unwrap_or_else(|e| e.exit());
std::process::exit(0)
}

/// Process the completion request
pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> {
pub fn try_complete(&self, cmd: &mut Command) -> clap::error::Result<()> {
debug!("CompleteCommand::try_complete: {:?}", self);
let CompleteCommand::Complete(complete) = self;
match complete {
CompleteShell::Bash(args) => Bash::try_complete(args, cmd),
CompleteShell::Fish(args) => Fish::try_complete(args, cmd),
CompleteShell::Register(RegisterArgs { path, shell }) => {
let script = shell.completion_script(cmd);
if path == std::path::Path::new("-") {
std::io::stdout().write_all(&script)?;
io::stdout().write_all(&script)?;
} else if path.is_dir() {
let path = path.join(shell.file_name(cmd.get_name()));
std::fs::write(path, script)?;
Expand All @@ -103,3 +115,40 @@ impl CompleteCommand {
}
}
}

/// All information relevant to producing the completions
pub struct CompletionContext {
/// The value that could be expected
value_for: Option<Arg>,
/// The flags that could be expected
///
/// Does not contain options that are already present or that conflict with other args.
/// Is empty when `value_for` is an option expecting a value
options: Vec<Arg>,
/// The subcommands that could be expected
subcommands: Vec<Command>,
}

/// Complete the command specified
pub fn complete(
cmd: &mut Command,
args: impl IntoIterator<Item = impl Into<OsString>>,
) -> io::Result<CompletionContext> {
cmd.build();
let current_dir = std::env::current_dir().ok();

let raw_args = clap_lex::RawArgs::new(args);
let mut cursor = raw_args.cursor();

// TODO: Multicall support
if !cmd.is_no_binary_name_set() {
raw_args.next_os(&mut cursor);
}

todo!("Implement parsing");
}

fn completions_for_arg(arg: clap::Arg) -> io::Result<Vec<(String, Option<String>)>> {
// TODO take current token to complete subdirectories
Ok(Vec::new())
}

0 comments on commit 890c93e

Please sign in to comment.