Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
use clap::{Arg, Command as ClapCommand};
|
||||
use warp_command_signatures::{
|
||||
Argument, IsArgumentOptional, Opt, ParserDirectives, Priority, Signature,
|
||||
};
|
||||
|
||||
/// Convert a [`clap::Command`] into a [`Signature`]. All subcommands, options, and arguments are
|
||||
/// preserved on a best-effort basis.
|
||||
pub fn signature_from_clap_command(cmd: &mut ClapCommand, bin_name: &str) -> Signature {
|
||||
cmd.set_bin_name(bin_name);
|
||||
// Building the command sets all sorts of derived properties like Args::get_num_args.
|
||||
cmd.build();
|
||||
convert_command(cmd, cmd.get_bin_name().expect("Set above").to_string())
|
||||
}
|
||||
|
||||
fn convert_command(cmd: &ClapCommand, name: String) -> Signature {
|
||||
let description = cmd.get_about().map(|s| s.to_string());
|
||||
|
||||
let arguments = convert_positional_args(cmd);
|
||||
let options = convert_options(cmd);
|
||||
let subcommands = convert_subcommands(cmd);
|
||||
|
||||
Signature {
|
||||
name,
|
||||
alias_generator: None,
|
||||
description,
|
||||
arguments,
|
||||
subcommands,
|
||||
options,
|
||||
priority: Priority::default(),
|
||||
parser_directives: ParserDirectives::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert clap positional arguments to Signature arguments.
|
||||
fn convert_positional_args(cmd: &ClapCommand) -> Option<Vec<Argument>> {
|
||||
let positional_args: Vec<Argument> = cmd
|
||||
.get_positionals()
|
||||
.filter(|arg| !arg.is_hide_set())
|
||||
.map(convert_arg_to_argument)
|
||||
.collect();
|
||||
|
||||
if positional_args.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(positional_args)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert clap options/flags to Signature options.
|
||||
fn convert_options(cmd: &ClapCommand) -> Option<Vec<Opt>> {
|
||||
let opts: Vec<Opt> = cmd
|
||||
.get_opts()
|
||||
.filter(|arg| !arg.is_positional() && !arg.is_hide_set())
|
||||
.map(convert_arg_to_opt)
|
||||
.collect();
|
||||
|
||||
if opts.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(opts)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert clap subcommands to Signature subcommands by recursively converting each subcommand.
|
||||
fn convert_subcommands(cmd: &ClapCommand) -> Option<Vec<Signature>> {
|
||||
let subcommands: Vec<Signature> = cmd
|
||||
.get_subcommands()
|
||||
.filter(|subcmd| !subcmd.is_hide_set())
|
||||
.flat_map(|cmd| {
|
||||
std::iter::once(cmd.get_name())
|
||||
.chain(cmd.get_visible_aliases())
|
||||
.map(|cmd_or_alias| convert_command(cmd, cmd_or_alias.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect();
|
||||
|
||||
if subcommands.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(subcommands)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [`clap::Arg`] to a positional [`Argument`].
|
||||
fn convert_arg_to_argument(arg: &Arg) -> Argument {
|
||||
let display_name = Some(arg.get_id().to_string());
|
||||
let description = arg.get_help().map(|s| s.to_string());
|
||||
let optional = if arg.is_required_set() {
|
||||
IsArgumentOptional::Required
|
||||
} else {
|
||||
IsArgumentOptional::Optional(
|
||||
arg.get_default_values()
|
||||
.first()
|
||||
.map(|s| s.to_string_lossy().to_string()),
|
||||
)
|
||||
};
|
||||
|
||||
Argument {
|
||||
display_name,
|
||||
description,
|
||||
is_variadic: arg.get_num_args().is_some_and(|num_args| {
|
||||
num_args.takes_values() && num_args.min_values() != num_args.max_values()
|
||||
}),
|
||||
// TODO: Extract argument types from clap value hints.
|
||||
argument_types: vec![],
|
||||
optional,
|
||||
is_command: false,
|
||||
skip_generator_validation: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [`clap::Arg]` to an [`Opt`] representing a flag/option.
|
||||
fn convert_arg_to_opt(arg: &Arg) -> Opt {
|
||||
let mut exact_string = Vec::new();
|
||||
|
||||
// Add short flags (e.g., "-h")
|
||||
for short in arg.get_short_and_visible_aliases().into_iter().flatten() {
|
||||
exact_string.push(format!("-{short}"));
|
||||
}
|
||||
|
||||
// Add long flags (e.g., "--help")
|
||||
for long in arg.get_long_and_visible_aliases().into_iter().flatten() {
|
||||
exact_string.push(format!("--{long}"));
|
||||
}
|
||||
|
||||
let description = arg.get_help().map(|s| s.to_string());
|
||||
let required = arg.is_required_set();
|
||||
|
||||
let arguments = arg.get_num_args().and_then(|num_args| {
|
||||
if num_args.takes_values() {
|
||||
// TODO: Handle multi-valued flags. The Clap documentation is fairly unclear on how
|
||||
// it models this (e.g. can we assume that get_default_values and get_value names are
|
||||
// paired? Why is there only one ValueHint?). We currently don't need support for this.
|
||||
|
||||
Some(vec![Argument {
|
||||
display_name: arg
|
||||
.get_value_names()
|
||||
.and_then(|names| names.first())
|
||||
.map(|s| s.to_string()),
|
||||
description: None,
|
||||
is_variadic: false,
|
||||
// TODO: Infer from ValuesHint.
|
||||
argument_types: vec![],
|
||||
optional: arg
|
||||
.get_default_values()
|
||||
.first()
|
||||
.map(|s| IsArgumentOptional::Optional(Some(s.to_string_lossy().to_string())))
|
||||
.unwrap_or(IsArgumentOptional::Required),
|
||||
is_command: false,
|
||||
skip_generator_validation: true,
|
||||
}])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
Opt {
|
||||
exact_string,
|
||||
description,
|
||||
arguments,
|
||||
required,
|
||||
priority: Priority::default(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use warp_core::channel::Channel;
|
||||
|
||||
pub mod registry;
|
||||
|
||||
pub use registry::CommandRegistry;
|
||||
#[cfg(feature = "test-util")]
|
||||
use warp_command_signatures::Signature;
|
||||
|
||||
static GLOBAL_REGISTRY: OnceLock<Arc<CommandRegistry>> = OnceLock::new();
|
||||
|
||||
impl CommandRegistry {
|
||||
/// Returns a reference to a single global instance of the command registry.
|
||||
///
|
||||
/// The registry is a read-only store of information used to provide smart
|
||||
/// suggestions and completions, and as such, only one instance is required
|
||||
/// across the application. The registry itself can be quite large, so use
|
||||
/// of a single global instance avoids unnecessary memory allocations and
|
||||
/// usage.
|
||||
pub fn global_instance() -> Arc<Self> {
|
||||
GLOBAL_REGISTRY
|
||||
.get_or_init(|| {
|
||||
// TODO(wasm): Determine how to asynchronously load command signatures on wasm.
|
||||
Arc::new(CommandRegistry::new_with_embedded_signatures())
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Returns a new [`CommandRegistry`] that looks up commands in the embedded
|
||||
/// set of command signatures.
|
||||
fn new_with_embedded_signatures() -> Self {
|
||||
let registry = CommandRegistry::new(
|
||||
|command| {
|
||||
let start = instant::Instant::now();
|
||||
let signature = warp_command_signatures::signature_by_name(command);
|
||||
log::debug!(
|
||||
"Lazily loaded command signature for {command} in {}s",
|
||||
start.elapsed().as_secs_f32()
|
||||
);
|
||||
signature
|
||||
},
|
||||
warp_command_signatures::dynamic_command_signature_data(),
|
||||
);
|
||||
|
||||
Self::register_warp_signatures(®istry);
|
||||
|
||||
registry
|
||||
}
|
||||
|
||||
/// Register signatures for Warp CLI commands.
|
||||
///
|
||||
/// Ideally this would be done outside of the `warp_completer` crate, but it's not currently
|
||||
/// possible to configure the shared [`Self::global_instance`].
|
||||
fn register_warp_signatures(registry: &Self) {
|
||||
// We use the current instance's signature for each channel. This is not entirely accurate - for example:
|
||||
// * The user might be SSHed into a host with a different version of the CLI
|
||||
// * The user might be using Preview, which will have different features than Stable.
|
||||
// However, it'll be close enough, and this approach ensures that we keep the CLI completions up to date.
|
||||
let channels = [Channel::Stable, Channel::Preview, Channel::Dev];
|
||||
|
||||
for channel in channels {
|
||||
let bin_name = channel.cli_command_name();
|
||||
let mut clap_cmd = warp_cli::Args::clap_command();
|
||||
let signature =
|
||||
crate::signatures::clap::signature_from_clap_command(&mut clap_cmd, bin_name);
|
||||
registry.register_signature(signature);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an empty [`CommandRegistry`] that contains no signatures nor
|
||||
/// generators.
|
||||
pub fn empty() -> Self {
|
||||
CommandRegistry::new(|_| None, std::collections::HashMap::new())
|
||||
}
|
||||
|
||||
/// Returns a [`CommandRegistry`] that uses the provided set of signatures
|
||||
/// and generators. This does not utilize any data from the
|
||||
/// warp-command-signatures crate.
|
||||
#[cfg(feature = "test-util")]
|
||||
pub fn new_for_test(
|
||||
signatures: impl IntoIterator<Item = Signature>,
|
||||
generators: std::collections::HashMap<
|
||||
String,
|
||||
warp_command_signatures::DynamicCompletionData,
|
||||
>,
|
||||
) -> Self {
|
||||
let registry = CommandRegistry::new(|_| None, generators);
|
||||
signatures
|
||||
.into_iter()
|
||||
.for_each(|signature| registry.register_signature(signature));
|
||||
registry
|
||||
}
|
||||
}
|
||||
|
||||
// We only implement Default for this in tests, as in production, we should
|
||||
// always use the shared instance, but in tests, we might want to configure
|
||||
// instances differently.
|
||||
#[cfg(feature = "test-util")]
|
||||
impl Default for CommandRegistry {
|
||||
fn default() -> Self {
|
||||
CommandRegistry::new_with_embedded_signatures()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
use crate::completer::{CommandExitStatus, CompletionContext, TopLevelCommandCaseSensitivity};
|
||||
use crate::parsers::SignatureAtTokenIndex;
|
||||
|
||||
use itertools::Itertools;
|
||||
use memo_map::MemoMap;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use warp_command_signatures::{Argument, DynamicCompletionData, IsArgumentOptional, Signature};
|
||||
|
||||
pub enum SignatureResult<'a> {
|
||||
/// Successfully parsed the signature. We are returning the signature at the token to complete on.
|
||||
Success(SignatureAtTokenIndex<'a>),
|
||||
/// The command contains an alias. We are returning the expanded command to re-run the parser.
|
||||
NeedAliasExpansion(String),
|
||||
/// Couldn't find a signature.
|
||||
None,
|
||||
}
|
||||
|
||||
type SignatureLookupFn = dyn 'static + Send + Sync + Fn(&str) -> Option<Signature>;
|
||||
|
||||
/// A simple structure to cache parsed command signatures. These are stored as
|
||||
/// JSON, so this makes it easy for us to lazily load and parse the JSON when
|
||||
/// a command signature is needed, and only need to do that parsing work once
|
||||
/// per signature per run of the program.
|
||||
struct SignatureCache {
|
||||
/// A function that, given the name of a command, returns the [`Signature`]
|
||||
/// for it. Should return None if there is no signature available for the
|
||||
/// given command.
|
||||
lookup_fn: Box<SignatureLookupFn>,
|
||||
/// A map from command name to the signature for the command, if any. The
|
||||
/// use of [`MemoMap`] here allows us to safely return references to the
|
||||
/// contained signatures (as the map internally is an append-only
|
||||
/// structure). This stores an `Option<Signature>` in order to also store
|
||||
/// our knowledge of commands for which we do _not_ have a signature.
|
||||
signatures: MemoMap<String, Option<Signature>>,
|
||||
}
|
||||
|
||||
impl SignatureCache {
|
||||
fn new(lookup_fn: Box<SignatureLookupFn>) -> Self {
|
||||
Self {
|
||||
lookup_fn,
|
||||
signatures: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, command: &str) -> Option<&Signature> {
|
||||
let command = if cfg!(windows) {
|
||||
command.trim_end_matches(".exe")
|
||||
} else {
|
||||
command
|
||||
};
|
||||
let command = command.to_lowercase();
|
||||
self.signatures
|
||||
.get_or_insert(&command, || (self.lookup_fn)(&command))
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
/// Inserts the given `Signature` into the underlying map, keyed by `Signature::name`.
|
||||
///
|
||||
/// If there is already a cached value for the given `Signature::name`, this is a no-op (even
|
||||
/// if the cached value is `None`).
|
||||
fn insert(&self, signature: Signature) {
|
||||
self.signatures
|
||||
.insert(signature.name.to_lowercase(), Some(signature));
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a wrapper around a HashMap<String, T> to enforce the invariant that all keys must be
|
||||
/// all lowercase letters.
|
||||
#[derive(Clone, Debug)]
|
||||
struct CaseInsensitiveHashMap<T> {
|
||||
map: HashMap<String, T>,
|
||||
}
|
||||
|
||||
impl<T> Default for CaseInsensitiveHashMap<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
map: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> CaseInsensitiveHashMap<T> {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn get(&self, key: &str) -> Option<&T> {
|
||||
self.map.get(&key.to_lowercase())
|
||||
}
|
||||
|
||||
fn insert(&mut self, key: &str, val: T) -> Option<T> {
|
||||
self.map.insert(key.to_lowercase(), val)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromIterator<(String, T)> for CaseInsensitiveHashMap<T> {
|
||||
fn from_iter<I: IntoIterator<Item = (String, T)>>(iter: I) -> Self {
|
||||
let mut map = Self::new();
|
||||
for (key, val) in iter.into_iter() {
|
||||
map.insert(&key, val);
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CommandRegistry {
|
||||
signatures: SignatureCache,
|
||||
dynamic_completion_data: CaseInsensitiveHashMap<DynamicCompletionData>,
|
||||
}
|
||||
|
||||
impl CommandRegistry {
|
||||
pub(super) fn new<F>(
|
||||
signature_lookup_fn: F,
|
||||
dynamic_completion_data: HashMap<String, DynamicCompletionData>,
|
||||
) -> CommandRegistry
|
||||
where
|
||||
F: 'static + Send + Sync + Fn(&str) -> Option<Signature>,
|
||||
{
|
||||
CommandRegistry {
|
||||
signatures: SignatureCache::new(Box::new(signature_lookup_fn)),
|
||||
dynamic_completion_data: dynamic_completion_data.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn registered_commands(&self) -> impl Iterator<Item = &str> {
|
||||
// Note we need to collect the keys because MemoMap uses a mutex under the hood to control
|
||||
// access to the underlying signature data. This means the mutex is locked as long as the
|
||||
// iterator returned from `keys()` lives, which means we need to collect keys into a vec
|
||||
// and return an owned iterator.
|
||||
self.signatures
|
||||
.signatures
|
||||
.iter()
|
||||
.filter_map(|(key, signature)| signature.as_ref().map(|_| key.as_str()))
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
pub fn signature_from_line(
|
||||
&self,
|
||||
line: &str,
|
||||
command_case_sensitivity: TopLevelCommandCaseSensitivity,
|
||||
) -> Option<SignatureAtTokenIndex<'_>> {
|
||||
let names = line.split_whitespace().collect_vec();
|
||||
self.signature_from_tokens(
|
||||
&names,
|
||||
line.ends_with(char::is_whitespace),
|
||||
command_case_sensitivity,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a replacement [`Signature`] and its corresponding [`DynamicCompletionData`] iff
|
||||
/// the current signature has an argument that should be a top level command and we are in a
|
||||
/// position where we would be completing on arguments.
|
||||
/// For example: if we had a token list of `sudo git ` (note the whitespace) we should return
|
||||
/// the `Signature` for `git`.
|
||||
///
|
||||
/// NOTE this function does not handle the case where the `Signature` has multiple arguments
|
||||
/// and an argument other than the first should be a top level command. Fig also does not
|
||||
/// support this case, see CORE-2154 for more details.
|
||||
fn maybe_load_replacement_signature(
|
||||
&self,
|
||||
signature: &Signature,
|
||||
tokens: &[&str],
|
||||
current_index: usize,
|
||||
token: &str,
|
||||
has_post_whitespace: bool,
|
||||
) -> Option<(&Signature, Option<&DynamicCompletionData>)> {
|
||||
if !signature.arguments().iter().any(Argument::is_command) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let is_last_token = tokens.len() - 1 == current_index;
|
||||
|
||||
let replacement_signature = self.signatures.get(token).map(|signature| {
|
||||
(
|
||||
signature,
|
||||
self.dynamic_completion_data.get(signature.name()),
|
||||
)
|
||||
})?;
|
||||
|
||||
if is_last_token {
|
||||
has_post_whitespace.then_some(replacement_signature)
|
||||
} else {
|
||||
Some(replacement_signature)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn signature_with_alias_expansion(
|
||||
&self,
|
||||
tokens: &[&str],
|
||||
has_post_whitespace: bool,
|
||||
context: &dyn CompletionContext,
|
||||
) -> SignatureResult<'_> {
|
||||
let found_signature = tokens.first().and_then(|command| {
|
||||
let command = if cfg!(windows) {
|
||||
command.trim_end_matches(".exe")
|
||||
} else {
|
||||
command
|
||||
};
|
||||
self.signatures
|
||||
.get(command)
|
||||
.map(|signature| (signature, self.dynamic_completion_data.get(command)))
|
||||
});
|
||||
|
||||
let Some((signature, mut dynamic_completion_data)) = found_signature else {
|
||||
return SignatureResult::None;
|
||||
};
|
||||
|
||||
let mut signature_start_idx = 0;
|
||||
let mut curr_signature = signature;
|
||||
|
||||
// Iterate through tokens after the top level command
|
||||
let mut token_idx = 1;
|
||||
while token_idx < tokens.len() {
|
||||
// If at last token, and there's no post-whitespace, don't actually try to resolve
|
||||
// this token as an alias since we're actually completing on that token itself.
|
||||
if token_idx == tokens.len() - 1 && !has_post_whitespace {
|
||||
break;
|
||||
}
|
||||
|
||||
let token = tokens[token_idx];
|
||||
// Check if there is any alias at the current signature.
|
||||
if let Some(alias) =
|
||||
curr_signature.alias(dynamic_completion_data.map(DynamicCompletionData::aliases))
|
||||
{
|
||||
// Get the shell command to execute for getting the alias.
|
||||
let command_to_run = alias.command(&tokens[..token_idx + 1]);
|
||||
|
||||
if let Some(generator_context) = context.generator_context() {
|
||||
if let Ok(output) = generator_context
|
||||
.execute_command_at_pwd(&command_to_run, None)
|
||||
.await
|
||||
{
|
||||
if let Ok(output_string) = output.to_string() {
|
||||
// If the command output was successful, attempt to complete on the alias.
|
||||
match output.status {
|
||||
CommandExitStatus::Success => {
|
||||
let expanded_command =
|
||||
alias.on_complete(&output_string, tokens, token_idx);
|
||||
|
||||
if let Some(expanded_command) = expanded_command {
|
||||
return SignatureResult::NeedAliasExpansion(
|
||||
expanded_command,
|
||||
);
|
||||
}
|
||||
}
|
||||
CommandExitStatus::Failure => {
|
||||
// We purposefully do not log an error here if the command failed because
|
||||
// many commands (such as `git`) will fail if there isn't a valid alias for
|
||||
// the token.
|
||||
log::debug!(
|
||||
"Execution of `{}` failed with output: {}",
|
||||
command_to_run,
|
||||
&output_string
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::debug!(
|
||||
"Execution of `{command_to_run}` returned an unparseable output",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((replacement_signature, replacement_completion_data)) = self
|
||||
.maybe_load_replacement_signature(
|
||||
signature,
|
||||
tokens,
|
||||
token_idx,
|
||||
token,
|
||||
has_post_whitespace,
|
||||
)
|
||||
{
|
||||
curr_signature = replacement_signature;
|
||||
dynamic_completion_data = replacement_completion_data;
|
||||
signature_start_idx = token_idx;
|
||||
} else {
|
||||
match classify_token(
|
||||
curr_signature,
|
||||
token,
|
||||
tokens.len(),
|
||||
token_idx,
|
||||
has_post_whitespace,
|
||||
) {
|
||||
TokenAction::ResolvedSubcommand { signature } => {
|
||||
curr_signature = signature;
|
||||
signature_start_idx = token_idx;
|
||||
}
|
||||
TokenAction::SkippedOption { advance_by } => {
|
||||
token_idx += advance_by;
|
||||
}
|
||||
TokenAction::SkippedUnrecognizedFlag => {}
|
||||
TokenAction::VariadicOption | TokenAction::StopAtCurrentToken => {
|
||||
return SignatureResult::Success(SignatureAtTokenIndex::new(
|
||||
curr_signature,
|
||||
dynamic_completion_data,
|
||||
signature_start_idx,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
token_idx += 1;
|
||||
}
|
||||
|
||||
SignatureResult::Success(SignatureAtTokenIndex::new(
|
||||
curr_signature,
|
||||
dynamic_completion_data,
|
||||
signature_start_idx,
|
||||
))
|
||||
}
|
||||
|
||||
/// Finds a signature from a list of tokens--returning the index of the token where the
|
||||
/// signature starts.
|
||||
pub fn signature_from_tokens(
|
||||
&self,
|
||||
tokens: &[&str],
|
||||
has_post_whitespace: bool,
|
||||
command_case_sensitivity: TopLevelCommandCaseSensitivity,
|
||||
) -> Option<SignatureAtTokenIndex<'_>> {
|
||||
let first_token = *tokens.first()?;
|
||||
|
||||
// Find the top level signature.
|
||||
let (signature, mut dynamic_completion_data) = self
|
||||
.signatures
|
||||
.get(first_token)
|
||||
.map(|signature| (signature, self.dynamic_completion_data.get(first_token)))?;
|
||||
|
||||
// Signature lookup is case-insensitive. However, sometimes we need to treat the lookup as
|
||||
// case-sensitive. There are 2 variables to check for that. The first is the
|
||||
// `command_case_sensitivity` parameter which represents the platform's filesystem
|
||||
// case-sensitivity. This, however, may be overridden by
|
||||
// `ParserDirectives::always_case_insensitive`. When that is true, we ignore the platform.
|
||||
// If we are treating this as a case-sensitive lookup, `signature.name` will contain the
|
||||
// canonical stylization of the name, and so we compare what the user typed, `first_token`,
|
||||
// to that.
|
||||
// For example, on Linux (case-sensitive by default), "GIT" should not match the spec for
|
||||
// "git". `first_token` will be "GIT" and `signature.name` will be "git". We return `None`.
|
||||
// However, if the user is running PowerShell and calls "set-location", this _should_ match
|
||||
// the spec for "Set-Location", so we skip the `signature.name != first_token` check. FYI
|
||||
// `signature.name` will be formatted as "Set-Location" as that is the preferred style.
|
||||
if command_case_sensitivity == TopLevelCommandCaseSensitivity::CaseSensitive
|
||||
&& !signature.parser_directives.always_case_insensitive
|
||||
&& signature.name != first_token
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut signature_start_idx = 0;
|
||||
let mut curr_signature = signature;
|
||||
|
||||
// Iterate through tokens after the top level command
|
||||
let mut token_idx = 1;
|
||||
while token_idx < tokens.len() {
|
||||
let token = tokens[token_idx];
|
||||
|
||||
if let Some((replacement_signature, replacement_completion_data)) = self
|
||||
.maybe_load_replacement_signature(
|
||||
signature,
|
||||
tokens,
|
||||
token_idx,
|
||||
token,
|
||||
has_post_whitespace,
|
||||
)
|
||||
{
|
||||
curr_signature = replacement_signature;
|
||||
dynamic_completion_data = replacement_completion_data;
|
||||
signature_start_idx = token_idx;
|
||||
} else {
|
||||
match classify_token(
|
||||
curr_signature,
|
||||
token,
|
||||
tokens.len(),
|
||||
token_idx,
|
||||
has_post_whitespace,
|
||||
) {
|
||||
TokenAction::ResolvedSubcommand { signature } => {
|
||||
curr_signature = signature;
|
||||
signature_start_idx = token_idx;
|
||||
}
|
||||
TokenAction::SkippedOption { advance_by } => {
|
||||
token_idx += advance_by;
|
||||
}
|
||||
TokenAction::SkippedUnrecognizedFlag => {}
|
||||
TokenAction::VariadicOption | TokenAction::StopAtCurrentToken => {
|
||||
return Some(SignatureAtTokenIndex::new(
|
||||
curr_signature,
|
||||
dynamic_completion_data,
|
||||
signature_start_idx,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
token_idx += 1;
|
||||
}
|
||||
|
||||
Some(SignatureAtTokenIndex::new(
|
||||
curr_signature,
|
||||
dynamic_completion_data,
|
||||
signature_start_idx,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn signature(&self, name: &str) -> Option<&Signature> {
|
||||
self.signatures.get(name)
|
||||
}
|
||||
|
||||
/// Registers the given `Signature`.
|
||||
///
|
||||
/// Note the underlying map caches the lookup result for a given signature (regardless of
|
||||
/// whether or not it is `Some` or `None`), which means that if there is already a cached
|
||||
/// `None` value for the command corresponding to this signature, this is a no-op.
|
||||
pub fn register_signature(&self, signature: Signature) {
|
||||
self.signatures.insert(signature);
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of classifying a single token during signature resolution.
|
||||
enum TokenAction<'a> {
|
||||
/// The token matched a subcommand of the current signature.
|
||||
ResolvedSubcommand { signature: &'a Signature },
|
||||
/// The token matched a recognized option whose last argument is variadic.
|
||||
/// The caller should stop walking tokens and return the current signature.
|
||||
VariadicOption,
|
||||
/// The token matched a recognized option with a fixed number of required
|
||||
/// arguments. The caller should advance `token_idx` by `advance_by` to skip
|
||||
/// past those arguments (the flag token itself is advanced separately).
|
||||
SkippedOption { advance_by: usize },
|
||||
/// The token starts with '-' but didn't match any recognized option.
|
||||
SkippedUnrecognizedFlag,
|
||||
/// The token is not a subcommand, option, or flag-like. The caller should
|
||||
/// stop walking tokens and return the current signature.
|
||||
StopAtCurrentToken,
|
||||
}
|
||||
|
||||
/// Classifies a token against the current signature's subcommands and options.
|
||||
///
|
||||
/// This encapsulates the shared per-token decision logic. Callers
|
||||
/// handle replacement signatures (e.g. `sudo git`) separately before
|
||||
/// invoking this function.
|
||||
fn classify_token<'a>(
|
||||
curr_signature: &'a Signature,
|
||||
token: &str,
|
||||
num_tokens: usize,
|
||||
token_idx: usize,
|
||||
has_post_whitespace: bool,
|
||||
) -> TokenAction<'a> {
|
||||
if let Some(subcommand) = curr_signature.subcommands().iter().find(|s| {
|
||||
should_complete_on_subcmd(s.name(), token, num_tokens, token_idx, has_post_whitespace)
|
||||
}) {
|
||||
return TokenAction::ResolvedSubcommand {
|
||||
signature: subcommand,
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(option) = find_option_by_name(curr_signature.options(), token) {
|
||||
if option.arguments().last().is_some_and(|arg| arg.is_variadic) {
|
||||
return TokenAction::VariadicOption;
|
||||
}
|
||||
let required_arg_count = option
|
||||
.arguments()
|
||||
.iter()
|
||||
.filter(|arg| arg.optional == IsArgumentOptional::Required)
|
||||
.count();
|
||||
return TokenAction::SkippedOption {
|
||||
advance_by: required_arg_count,
|
||||
};
|
||||
}
|
||||
|
||||
if token.starts_with('-') {
|
||||
return TokenAction::SkippedUnrecognizedFlag;
|
||||
}
|
||||
|
||||
TokenAction::StopAtCurrentToken
|
||||
}
|
||||
|
||||
/// Finds an option by exact name match against the token.
|
||||
fn find_option_by_name<'a>(
|
||||
options: &'a [warp_command_signatures::Opt],
|
||||
token: &str,
|
||||
) -> Option<&'a warp_command_signatures::Opt> {
|
||||
options
|
||||
.iter()
|
||||
.find(|option| option.exact_string.iter().any(|s| s == token))
|
||||
}
|
||||
|
||||
/// Returns true iff we should resolve the subcmd as a new [`Signature`].
|
||||
///
|
||||
/// If token is the last token, then as long as the subcmd matches the token name and there's whitespace at the end,
|
||||
/// then we should complete on the signature. If there wasn't whitespace at the end, then we would actually want to
|
||||
/// complete on the subcmds, and not resolve this subcmd. For example, suppoes the line is 'npm r' vs 'npm r '.
|
||||
/// In the former, we want to find `npm` subcommand completions. In the latter, we want to find `npm r` completions.
|
||||
///
|
||||
/// Otherwise, we are not at the last token so we should recursively resolve as long as the subcmd matches the token.
|
||||
fn should_complete_on_subcmd(
|
||||
subcmd_name: &str,
|
||||
token: &str,
|
||||
num_tokens: usize,
|
||||
curr_token_idx: usize,
|
||||
has_post_whitespace: bool,
|
||||
) -> bool {
|
||||
let is_last_token = num_tokens - 1 == curr_token_idx;
|
||||
let subcmd_matches_token = subcmd_name == token;
|
||||
|
||||
if is_last_token {
|
||||
has_post_whitespace && subcmd_matches_token
|
||||
} else {
|
||||
subcmd_matches_token
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "registry_test.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,234 @@
|
||||
use crate::completer::testing::FakeCompletionContext;
|
||||
use crate::completer::CompletionContext;
|
||||
use crate::completer::TopLevelCommandCaseSensitivity;
|
||||
use crate::signatures::registry::SignatureResult;
|
||||
use crate::signatures::testing::{create_test_command_registry, test_signature};
|
||||
|
||||
#[test]
|
||||
fn test_find_command_from_a_top_level_signature() {
|
||||
let bundle = warp_command_signatures::signature_by_name("bundle")
|
||||
.expect("global command signatures should include 'bundle'");
|
||||
|
||||
let registry = create_test_command_registry([bundle.clone(), test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"bundle exec ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.map(|c| c.signature);
|
||||
let signature = bundle;
|
||||
let exec_subcommand = signature
|
||||
.subcommands()
|
||||
.iter()
|
||||
.find(|sig| sig.name() == "exec");
|
||||
assert_eq!(found_signature, exec_subcommand);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_subcommand_signature_with_flags() {
|
||||
let kubectl = warp_command_signatures::signature_by_name("kubectl")
|
||||
.expect("global command signatures should include 'kubectl'");
|
||||
|
||||
let registry = create_test_command_registry([kubectl.clone(), test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"kubectl -n default get ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("kubectl signature from line should exist");
|
||||
// Should parse this as entering the "get" subcommand even though there's a top level -n default flag.
|
||||
assert_eq!(found_signature.signature.name(), "get");
|
||||
assert_eq!(found_signature.token_index, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_option_by_name_exact_match_does_not_match_substring() {
|
||||
// Regression test: "-n" should match the "-n"/"--namespace" option, NOT
|
||||
// "--no-headers" (which contains the substring "-n"). The fix uses exact
|
||||
// equality instead of `contains`.
|
||||
let kubectl = warp_command_signatures::signature_by_name("kubectl")
|
||||
.expect("global command signatures should include 'kubectl'");
|
||||
|
||||
let registry = create_test_command_registry([kubectl, test_signature()]);
|
||||
|
||||
// "kubectl -n default api-resources " should resolve to "api-resources",
|
||||
// which has a "--no-headers" option. If "-n" incorrectly matched
|
||||
// "--no-headers" via substring, the parser would skip "default" as the
|
||||
// flag argument and never reach the "api-resources" subcommand.
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"kubectl -n default api-resources ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("kubectl signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "api-resources");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flag_arg_consumes_token_matching_subcommand_name() {
|
||||
// When a recognized flag takes a required argument, the next token should be consumed
|
||||
// as that flag's argument even if it happens to match a subcommand name.
|
||||
// Here, --not-long takes 1 argument, so "one" is consumed as that argument
|
||||
// rather than being resolved as the "one" subcommand.
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"test --not-long one foo ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("test signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "test");
|
||||
assert_eq!(found_signature.token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_switches_before_subcommand() {
|
||||
// Multiple switch flags (no arguments) before a subcommand should all be
|
||||
// skipped, allowing the subcommand to be discovered.
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"test -r -V one foo ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("test signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "one");
|
||||
assert_eq!(found_signature.token_index, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unrecognized_flag_skipped_before_subcommand() {
|
||||
// Unrecognized flags (tokens starting with '-' not in the spec) should be
|
||||
// skipped so the parser can still discover subcommands after them.
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"test --unknown-flag one foo ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("test signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "one");
|
||||
assert_eq!(found_signature.token_index, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flag_with_missing_value_at_end_of_input() {
|
||||
// When a flag that takes a required argument appears at the end of input
|
||||
// with no value provided, the resolved signature stays on the parent command.
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"test --not-long ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("test signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "test");
|
||||
assert_eq!(found_signature.token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_flags_with_values_before_subcommand() {
|
||||
// Multiple valued flags before a subcommand should all be skipped,
|
||||
// allowing the subcommand to be discovered.
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
// --not-long takes 1 required arg ("val"), -r is a switch.
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"test --not-long val -r one foo ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("test signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "one");
|
||||
assert_eq!(found_signature.token_index, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_only_flags_no_subcommand() {
|
||||
// When only flags appear after the command with no following subcommand,
|
||||
// the parent command should be returned.
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"test --not-long val ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("test signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "test");
|
||||
assert_eq!(found_signature.token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_flag_arg_does_not_consume_subcommand() {
|
||||
// --required-and-optional-args has 1 required arg + 1 optional arg.
|
||||
// The parser should only skip the required arg, so "one" is found as a
|
||||
// subcommand rather than being consumed as the optional arg.
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"test --required-and-optional-args val one foo ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("test signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "one");
|
||||
assert_eq!(found_signature.token_index, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alias_expansion_path_skips_flag_with_value_before_subcommand() {
|
||||
// Exercises signature_with_alias_expansion (not just signature_from_tokens)
|
||||
// to ensure the alias-expansion code path also skips flags before subcommands.
|
||||
let kubectl = warp_command_signatures::signature_by_name("kubectl")
|
||||
.expect("global command signatures should include 'kubectl'");
|
||||
|
||||
let registry = create_test_command_registry([kubectl]);
|
||||
let ctx = FakeCompletionContext::new(registry).with_case_sensitivity();
|
||||
|
||||
let result = warpui::r#async::block_on(ctx.command_registry().signature_with_alias_expansion(
|
||||
&["kubectl", "-n", "default", "get"],
|
||||
true,
|
||||
&ctx,
|
||||
));
|
||||
let SignatureResult::Success(found_signature) = result else {
|
||||
panic!("expected SignatureResult::Success");
|
||||
};
|
||||
assert_eq!(found_signature.signature.name(), "get");
|
||||
assert_eq!(found_signature.token_index, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alias_expansion_path_skips_multiple_flags_before_subcommand() {
|
||||
// Exercises signature_with_alias_expansion with multiple flags (valued and
|
||||
// switch) placed before the subcommand.
|
||||
let kubectl = warp_command_signatures::signature_by_name("kubectl")
|
||||
.expect("global command signatures should include 'kubectl'");
|
||||
|
||||
let registry = create_test_command_registry([kubectl]);
|
||||
let ctx = FakeCompletionContext::new(registry).with_case_sensitivity();
|
||||
|
||||
let result = warpui::r#async::block_on(ctx.command_registry().signature_with_alias_expansion(
|
||||
&[
|
||||
"kubectl",
|
||||
"--context",
|
||||
"staging-cluster",
|
||||
"-n",
|
||||
"project1",
|
||||
"get",
|
||||
],
|
||||
true,
|
||||
&ctx,
|
||||
));
|
||||
let SignatureResult::Success(found_signature) = result else {
|
||||
panic!("expected SignatureResult::Success");
|
||||
};
|
||||
assert_eq!(found_signature.signature.name(), "get");
|
||||
assert_eq!(found_signature.token_index, 5);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#[cfg_attr(feature = "v2", path = "v2/mod.rs")]
|
||||
#[cfg_attr(not(feature = "v2"), path = "legacy/mod.rs")]
|
||||
mod imp;
|
||||
|
||||
pub use imp::*;
|
||||
|
||||
pub mod clap;
|
||||
|
||||
#[cfg(feature = "test-util")]
|
||||
pub mod testing;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
use super::CommandRegistry;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "v2")] {
|
||||
mod v2;
|
||||
pub use v2::*;
|
||||
|
||||
pub fn create_test_command_registry(
|
||||
signatures: impl IntoIterator<Item = super::CommandSignature>,
|
||||
) -> CommandRegistry {
|
||||
let registry = CommandRegistry::new();
|
||||
for signature in signatures.into_iter() {
|
||||
registry.register_signature(signature);
|
||||
}
|
||||
registry
|
||||
}
|
||||
} else if #[cfg(not(feature = "v2"))]{
|
||||
pub(crate) mod legacy;
|
||||
|
||||
pub use legacy::*;
|
||||
|
||||
pub fn create_test_command_registry(
|
||||
signatures: impl IntoIterator<Item = warp_command_signatures::Signature>,
|
||||
) -> CommandRegistry {
|
||||
use std::collections::HashMap;
|
||||
|
||||
let generators = HashMap::from([test_generators().into()]);
|
||||
CommandRegistry::new_for_test(signatures, generators)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const TEST_GENERATOR_1_COMMAND: &str = "echo 1";
|
||||
pub(crate) const TEST_GENERATOR_2_COMMAND: &str = "echo 2";
|
||||
pub(crate) const TEST_ALIAS_COMMAND: &str = "echo alias";
|
||||
@@ -0,0 +1,666 @@
|
||||
//! V2 versions of command signatures used for testing.
|
||||
//!
|
||||
//! Each signature in this file should be semantically equivalent with a command signature returned
|
||||
//! by a function of the same name in `super::legacy`; this is to ensure that the same test
|
||||
//! coverage can run with the "v2" Cargo feature both enabled and disabled.
|
||||
use warp_js::TypedJsFunctionRef;
|
||||
|
||||
use crate::signatures::{
|
||||
Argument, ArgumentValue, Arity, Command, CommandSignature, GeneratorFn, GeneratorResults,
|
||||
GeneratorScript, Opt, Priority, Suggestion, TemplateType,
|
||||
};
|
||||
|
||||
use super::{TEST_GENERATOR_1_COMMAND, TEST_GENERATOR_2_COMMAND};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub(crate) static ref TEST_GENERATOR_1_JS_FUNCTION: TypedJsFunctionRef<String, GeneratorResults> = TypedJsFunctionRef::<String, GeneratorResults>::new_for_test();
|
||||
pub(crate) static ref TEST_GENERATOR_2_JS_FUNCTION: TypedJsFunctionRef<String, GeneratorResults> = TypedJsFunctionRef::<String, GeneratorResults>::new_for_test();
|
||||
|
||||
static ref TEST_GENERATOR_1: ArgumentValue = ArgumentValue::Generator(GeneratorFn::ShellCommand {
|
||||
script: GeneratorScript::Static(TEST_GENERATOR_1_COMMAND.to_owned()),
|
||||
post_process: Some(TEST_GENERATOR_1_JS_FUNCTION.clone()),
|
||||
});
|
||||
|
||||
static ref TEST_GENERATOR_2: ArgumentValue = ArgumentValue::Generator(GeneratorFn::ShellCommand {
|
||||
script: GeneratorScript::Static(TEST_GENERATOR_2_COMMAND.to_owned()),
|
||||
post_process: Some(TEST_GENERATOR_2_JS_FUNCTION.clone()),
|
||||
});
|
||||
}
|
||||
|
||||
fn create_argument_value(name: impl Into<String>) -> ArgumentValue {
|
||||
ArgumentValue::Suggestion(Suggestion {
|
||||
value: name.into(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn create_argument_value_with_priority(
|
||||
name: impl Into<String>,
|
||||
priority: Priority,
|
||||
) -> ArgumentValue {
|
||||
ArgumentValue::Suggestion(Suggestion {
|
||||
value: name.into(),
|
||||
priority,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(zachbai): Use this function to create hidden suggestions when hidden suggestions are
|
||||
// implemented in V2.
|
||||
#[allow(dead_code)]
|
||||
fn create_hidden_argument_suggestion(name: impl Into<String>) -> ArgumentValue {
|
||||
ArgumentValue::Suggestion(Suggestion {
|
||||
value: name.into(),
|
||||
display_value: None,
|
||||
description: None,
|
||||
priority: Priority::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn test_signature() -> CommandSignature {
|
||||
CommandSignature {
|
||||
command: Command {
|
||||
name: "test".to_owned(),
|
||||
alias: vec!["alias".to_owned()],
|
||||
description: Some("testing...".to_owned()),
|
||||
subcommands: vec![
|
||||
Command {
|
||||
name: "one".to_owned(),
|
||||
arguments: vec![
|
||||
Argument {
|
||||
name: "first arg".to_owned(),
|
||||
values: vec![
|
||||
create_argument_value("one-one"),
|
||||
create_argument_value("one-two"),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
Argument {
|
||||
name: "second arg".to_owned(),
|
||||
values: vec![
|
||||
create_argument_value("two-one"),
|
||||
create_argument_value("two-two"),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
priority: Priority::max(),
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "two".to_owned(),
|
||||
arguments: vec![
|
||||
Argument {
|
||||
name: "two-one".to_owned(),
|
||||
arity: Some(Arity {
|
||||
limit: None,
|
||||
delimiter: None,
|
||||
}),
|
||||
values: vec![create_argument_value("two-one")],
|
||||
..Default::default()
|
||||
},
|
||||
Argument {
|
||||
name: "two-two".to_owned(),
|
||||
values: vec![create_argument_value("two-two")],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
priority: Priority::new(-50),
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "three".to_owned(),
|
||||
arguments: vec![
|
||||
Argument {
|
||||
name: "three-one".to_owned(),
|
||||
values: vec![create_argument_value("three-one")],
|
||||
..Default::default()
|
||||
},
|
||||
Argument {
|
||||
name: "three-two".to_owned(),
|
||||
values: vec![create_argument_value("three-two")],
|
||||
..Default::default()
|
||||
},
|
||||
Argument {
|
||||
name: "three-three".to_owned(),
|
||||
values: vec![create_argument_value("three-three")],
|
||||
optional: true,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
priority: Priority::min(),
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "four".to_owned(),
|
||||
arguments: vec![
|
||||
Argument {
|
||||
name: "four-one".to_owned(),
|
||||
values: vec![create_argument_value("four-one")],
|
||||
..Default::default()
|
||||
},
|
||||
Argument {
|
||||
name: "four-two".to_owned(),
|
||||
arity: Some(Arity {
|
||||
limit: None,
|
||||
delimiter: None,
|
||||
}),
|
||||
values: vec![create_argument_value("four-two")],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "five".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "five".to_owned(),
|
||||
values: vec![TEST_GENERATOR_1.clone(), TEST_GENERATOR_2.clone()],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "six".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "six-one".to_owned(),
|
||||
values: vec![
|
||||
create_argument_value("six-arg"),
|
||||
create_argument_value_with_priority("six-arg-2", Priority::max()),
|
||||
],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "seven".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "seven-arg".to_owned(),
|
||||
values: vec![TEST_GENERATOR_2.clone(), TEST_GENERATOR_2.clone()],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "eight".to_owned(),
|
||||
subcommands: vec![Command {
|
||||
name: "eight-subcommand".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
arguments: vec![
|
||||
Argument {
|
||||
name: "eight-arg".to_owned(),
|
||||
values: vec![create_argument_value("eight-arg")],
|
||||
..Default::default()
|
||||
},
|
||||
Argument {
|
||||
name: "eight-arg-2".to_owned(),
|
||||
arity: Some(Arity {
|
||||
limit: None,
|
||||
delimiter: None,
|
||||
}),
|
||||
values: vec![create_argument_value("eight-arg-2")],
|
||||
optional: true,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "nine".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "nine-arg".to_owned(),
|
||||
values: vec![create_argument_value("git")],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
options: vec![
|
||||
Opt {
|
||||
name: vec!["--long".to_owned()],
|
||||
arguments: vec![Argument {
|
||||
name: "long-one".to_owned(),
|
||||
arity: Some(Arity {
|
||||
limit: None,
|
||||
delimiter: None,
|
||||
}),
|
||||
values: vec![
|
||||
create_argument_value("long-one"),
|
||||
create_argument_value("long-two"),
|
||||
],
|
||||
..Default::default()
|
||||
}],
|
||||
priority: Priority::min(),
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec!["--not-long".to_owned()],
|
||||
arguments: vec![Argument {
|
||||
name: "long-one".to_owned(),
|
||||
values: vec![
|
||||
create_argument_value("not-long-one"),
|
||||
create_argument_value("not-long-two"),
|
||||
],
|
||||
..Default::default()
|
||||
}],
|
||||
priority: Priority::max(),
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec!["--required-args".to_owned()],
|
||||
arguments: vec![
|
||||
Argument {
|
||||
name: "required-arg-1".to_owned(),
|
||||
values: vec![
|
||||
create_argument_value("arg-1-1"),
|
||||
create_argument_value("arg-1-2"),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
Argument {
|
||||
name: "required-arg-2".to_owned(),
|
||||
values: vec![
|
||||
create_argument_value("arg-2-1"),
|
||||
create_argument_value("arg-2-2"),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
priority: Priority::max(),
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec!["--required-args-with-var".to_owned()],
|
||||
arguments: vec![
|
||||
Argument {
|
||||
name: "required-arg".to_owned(),
|
||||
values: vec![
|
||||
create_argument_value("arg-1"),
|
||||
create_argument_value("arg-2"),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
Argument {
|
||||
name: "variadic-arg".to_owned(),
|
||||
arity: Some(Arity {
|
||||
limit: None,
|
||||
delimiter: None,
|
||||
}),
|
||||
values: vec![
|
||||
create_argument_value("vararg-1"),
|
||||
create_argument_value("vararg-2"),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
priority: Priority::min(),
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec!["--required-and-optional-args".to_owned()],
|
||||
arguments: vec![
|
||||
Argument {
|
||||
name: "required-arg".to_owned(),
|
||||
values: vec![
|
||||
create_argument_value("required-1"),
|
||||
create_argument_value("required-2"),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
Argument {
|
||||
name: "optional-arg".to_owned(),
|
||||
values: vec![
|
||||
create_argument_value("optional-1"),
|
||||
create_argument_value("optional-2"),
|
||||
],
|
||||
optional: true,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
required: false,
|
||||
priority: Priority::default(),
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec!["--template-args-for-opt".to_owned()],
|
||||
arguments: vec![Argument {
|
||||
name: "templated".to_owned(),
|
||||
values: vec![ArgumentValue::Template {
|
||||
type_name: TemplateType::FilesAndFolders,
|
||||
filter_name: None,
|
||||
}],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec!["-r".to_owned()],
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec!["-V".to_owned()],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cd_signature() -> CommandSignature {
|
||||
CommandSignature {
|
||||
command: Command {
|
||||
name: "cd".to_owned(),
|
||||
alias: vec![],
|
||||
description: Some("testing...".to_owned()),
|
||||
arguments: vec![Argument {
|
||||
name: "directories".to_owned(),
|
||||
description: None,
|
||||
arity: None,
|
||||
values: vec![
|
||||
// TODO(completions-v2): Uncomment when "hidden" suggestions are implemented.
|
||||
// A "hidden" suggestion is only shown if it is an exact match for the current
|
||||
// token. In this case, "-" is only shown as a suggestion if the user has
|
||||
// exactly typed "cd -" in the input.
|
||||
// create_hidden_argument_suggestion('-'),
|
||||
ArgumentValue::Template {
|
||||
type_name: TemplateType::Folders,
|
||||
filter_name: None,
|
||||
},
|
||||
],
|
||||
optional: false,
|
||||
}],
|
||||
subcommands: vec![],
|
||||
options: vec![],
|
||||
priority: Priority::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ls_signature() -> CommandSignature {
|
||||
CommandSignature {
|
||||
command: Command {
|
||||
name: "ls".to_owned(),
|
||||
alias: vec![],
|
||||
description: Some("testing...".to_owned()),
|
||||
arguments: vec![Argument {
|
||||
name: "filepaths".to_owned(),
|
||||
description: None,
|
||||
arity: Some(Arity {
|
||||
limit: None,
|
||||
delimiter: None,
|
||||
}),
|
||||
values: vec![ArgumentValue::Template {
|
||||
type_name: TemplateType::FilesAndFolders,
|
||||
filter_name: None,
|
||||
}],
|
||||
optional: true,
|
||||
}],
|
||||
subcommands: vec![],
|
||||
options: vec![
|
||||
Opt {
|
||||
name: vec!["-a".to_owned()],
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec!["--color".to_owned()],
|
||||
arguments: vec![Argument {
|
||||
name: "when".to_owned(),
|
||||
values: vec![
|
||||
create_argument_value("force"),
|
||||
create_argument_value("auto"),
|
||||
create_argument_value("never"),
|
||||
],
|
||||
optional: true,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec!["--test".to_owned()],
|
||||
arguments: vec![Argument {
|
||||
name: "when".to_owned(),
|
||||
arity: Some(Arity {
|
||||
limit: None,
|
||||
delimiter: None,
|
||||
}),
|
||||
values: vec![
|
||||
create_argument_value("force"),
|
||||
create_argument_value("auto"),
|
||||
create_argument_value("never"),
|
||||
],
|
||||
optional: true,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
priority: Priority::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// A signature with a single positional that has no argument types.
|
||||
pub fn signature_with_empty_positional() -> CommandSignature {
|
||||
CommandSignature {
|
||||
command: Command {
|
||||
name: "test-empty".to_owned(),
|
||||
alias: vec![],
|
||||
description: Some("testing...".to_owned()),
|
||||
arguments: vec![Argument {
|
||||
name: "test-empty--arg".to_owned(),
|
||||
description: None,
|
||||
arity: None,
|
||||
values: vec![],
|
||||
optional: false,
|
||||
}],
|
||||
subcommands: vec![],
|
||||
options: vec![],
|
||||
priority: Priority::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn git_signature() -> CommandSignature {
|
||||
CommandSignature {
|
||||
command: Command {
|
||||
name: "git".to_owned(),
|
||||
description: Some("the stupid content tracker".to_owned()),
|
||||
subcommands: vec![
|
||||
Command {
|
||||
name: "add".to_owned(),
|
||||
description: Some("Add file contents to the index".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "checkout".to_owned(),
|
||||
description: Some("Switch branches or restore working tree files".to_owned()),
|
||||
arguments: vec![Argument {
|
||||
name: "branch".to_owned(),
|
||||
description: Some("Branch".to_owned()),
|
||||
values: vec![
|
||||
create_argument_value("漢字"),
|
||||
create_argument_value("bob/卡b卡"),
|
||||
],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "clone".to_owned(),
|
||||
description: Some("Clone a repository into a new directory".into()),
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "branch".to_owned(),
|
||||
description: Some("List, create, or delete branches".into()),
|
||||
options: vec![
|
||||
Opt {
|
||||
name: vec!["--delete".to_owned()],
|
||||
arguments: vec![Argument {
|
||||
name: "branch".to_owned(),
|
||||
arity: Some(Arity {
|
||||
limit: None,
|
||||
delimiter: None,
|
||||
}),
|
||||
values: vec![
|
||||
create_argument_value("branch-1"),
|
||||
create_argument_value("second-branch"),
|
||||
],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec!["-m".to_owned()],
|
||||
arguments: vec![
|
||||
Argument {
|
||||
name: "from_branch".to_owned(),
|
||||
values: vec![
|
||||
create_argument_value("branch-1"),
|
||||
create_argument_value("second-branch"),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
Argument {
|
||||
name: "to_branch".to_owned(),
|
||||
values: vec![
|
||||
create_argument_value("branch-1"),
|
||||
create_argument_value("second-branch"),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
options: vec![
|
||||
Opt {
|
||||
name: vec!["-p".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec!["--version".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec!["--help".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec!["--bare".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec!["-c".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn java_signature() -> CommandSignature {
|
||||
CommandSignature {
|
||||
command: Command {
|
||||
name: "java".to_string(),
|
||||
description: Some("Launch a java application".into()),
|
||||
arguments: vec![Argument {
|
||||
name: "<mainclass>".to_string(),
|
||||
optional: true,
|
||||
..Default::default()
|
||||
}],
|
||||
options: vec![
|
||||
Opt {
|
||||
// Java supports both styles of long-hand options.
|
||||
name: vec!["-version".to_string(), "--version".to_string()],
|
||||
description: Some("print product version to the error stream and exit".into()),
|
||||
..Default::default()
|
||||
},
|
||||
Opt {
|
||||
name: vec![
|
||||
"-cp".to_string(),
|
||||
"-classpath".to_string(),
|
||||
"--class-path".to_string(),
|
||||
],
|
||||
description: Some(
|
||||
"class search path of directories and zip/jar files".to_string(),
|
||||
),
|
||||
arguments: vec![Argument {
|
||||
name: "classpath".to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
required: false,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fuzzy_signature() -> CommandSignature {
|
||||
CommandSignature {
|
||||
command: Command {
|
||||
name: "fuzzy".to_owned(),
|
||||
description: Some("testing...".to_owned()),
|
||||
arguments: vec![],
|
||||
subcommands: vec![
|
||||
Command {
|
||||
name: "prefix1".to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "prefix2".to_owned(),
|
||||
priority: Priority::max(),
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "suffix-pre-fix".to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
options: vec![Opt {
|
||||
name: vec!["--pre-fx".to_owned()],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn npm_signature() -> CommandSignature {
|
||||
CommandSignature {
|
||||
command: Command {
|
||||
name: "npm".to_owned(),
|
||||
description: Some("testing...".to_owned()),
|
||||
subcommands: vec![
|
||||
Command {
|
||||
name: "r".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "r-arg".to_owned(),
|
||||
values: vec![create_argument_value("r-arg")],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "run".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "run-arg".to_owned(),
|
||||
values: vec![create_argument_value("run-arg")],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
options: vec![],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
//! Contains `FromWarpJs` trait implementations for converting JavaScript command signatures to
|
||||
//! `warp_completer::signatures::CommandSignature`s, as well as `IntoWarpJs` implementations for
|
||||
//! Rust structs that may be passed to JS functions defined on the Command Signature (e.g.
|
||||
//! `GeneratorCompletionContext`).
|
||||
use rquickjs::{FromJs, Function, Object, Value};
|
||||
use warp_js::{
|
||||
util::{get_one_or_more_optional, get_one_or_more_required, get_optional, get_required},
|
||||
FromWarpJs, IntoWarpJs, JsFunctionRegistry,
|
||||
};
|
||||
|
||||
use super::{
|
||||
Argument, ArgumentValue, Command, CommandSignature, GeneratorCompletionContext, GeneratorFn,
|
||||
GeneratorResults, GeneratorScript, Opt, Priority, Suggestion, TemplateType,
|
||||
};
|
||||
|
||||
impl<'js> FromWarpJs<'js> for CommandSignature {
|
||||
fn from_warp_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
let object = Object::from_value(value)?;
|
||||
let command = Command::from_warp_js(ctx, object.get("command")?, js_function_registry)?;
|
||||
Ok(Self { command })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for Command {
|
||||
fn from_warp_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
let object = Object::from_value(value)?;
|
||||
let name: String = get_required(&object, "name", js_function_registry, ctx)?;
|
||||
let alias: Vec<String> =
|
||||
get_one_or_more_optional(&object, "alias", js_function_registry, ctx)?;
|
||||
let description: Option<String> =
|
||||
get_optional(&object, "description", js_function_registry, ctx)?;
|
||||
let arguments: Vec<Argument> =
|
||||
get_one_or_more_optional(&object, "arguments", js_function_registry, ctx)?;
|
||||
let subcommands: Vec<Command> =
|
||||
get_one_or_more_optional(&object, "subcommands", js_function_registry, ctx)?;
|
||||
let options: Vec<Opt> =
|
||||
get_one_or_more_optional(&object, "options", js_function_registry, ctx)?;
|
||||
let priority: Option<i32> = get_optional(&object, "priority", js_function_registry, ctx)?;
|
||||
|
||||
Ok(Command {
|
||||
name,
|
||||
alias,
|
||||
description,
|
||||
arguments,
|
||||
subcommands,
|
||||
options,
|
||||
priority: priority.map(Priority::new).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for Argument {
|
||||
fn from_warp_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
let object = Object::from_value(value)?;
|
||||
let name: String = get_required(&object, "name", js_function_registry, ctx)?;
|
||||
let description: Option<String> =
|
||||
get_optional(&object, "description", js_function_registry, ctx)?;
|
||||
let values: Vec<ArgumentValue> =
|
||||
get_one_or_more_optional(&object, "values", js_function_registry, ctx)?;
|
||||
let optional: bool =
|
||||
get_optional(&object, "optional", js_function_registry, ctx)?.unwrap_or(false);
|
||||
|
||||
Ok(Argument {
|
||||
name,
|
||||
description,
|
||||
values,
|
||||
optional,
|
||||
arity: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for ArgumentValue {
|
||||
fn from_warp_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
// TODO(zachbai): Implement conversion + Rust representation of ArgumentValue.RootCommand
|
||||
// (see typescript schema in command-signature.d.ts).
|
||||
if value.is_object() {
|
||||
let object = Object::from_value(value)?;
|
||||
if object.contains_key("value")? {
|
||||
Ok(ArgumentValue::Suggestion(Suggestion::from_warp_js(
|
||||
ctx,
|
||||
object.into_value(),
|
||||
js_function_registry,
|
||||
)?))
|
||||
} else if let Some(type_name) =
|
||||
get_optional::<TemplateType>(&object, "typeName", js_function_registry, ctx)?
|
||||
{
|
||||
let filter_name: Option<String> =
|
||||
get_optional(&object, "filterName", js_function_registry, ctx)?;
|
||||
Ok(ArgumentValue::Template {
|
||||
type_name,
|
||||
filter_name,
|
||||
})
|
||||
} else if let Some(generate_suggestions_fn) = get_optional::<GeneratorFn>(
|
||||
&object,
|
||||
"generateSuggestionsFn",
|
||||
js_function_registry,
|
||||
ctx,
|
||||
)? {
|
||||
Ok(ArgumentValue::Generator(generate_suggestions_fn))
|
||||
} else {
|
||||
Err(rquickjs::Error::FromJs {
|
||||
from: "object",
|
||||
to: "ArgumentValue",
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Err(rquickjs::Error::FromJs {
|
||||
from: "object",
|
||||
to: "ArgumentValue",
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for Suggestion {
|
||||
fn from_warp_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
let object = Object::from_value(value)?;
|
||||
let value: String = get_required(&object, "value", js_function_registry, ctx)?;
|
||||
let display_value: Option<String> =
|
||||
get_optional(&object, "displayValue", js_function_registry, ctx)?;
|
||||
let description: Option<String> =
|
||||
get_optional(&object, "description", js_function_registry, ctx)?;
|
||||
let priority: Option<i32> = get_optional(&object, "priority", js_function_registry, ctx)?;
|
||||
|
||||
Ok(Suggestion {
|
||||
value,
|
||||
display_value,
|
||||
description,
|
||||
priority: priority.map(Priority::new).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for TemplateType {
|
||||
fn from_warp_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
let type_string = String::from_warp_js(ctx, value, js_function_registry)?;
|
||||
match type_string.as_str() {
|
||||
"TemplateType.Files" => Ok(TemplateType::Files),
|
||||
"TemplateType.Folders" => Ok(TemplateType::Folders),
|
||||
"TemplateType.FilesAndFolders" => Ok(TemplateType::FilesAndFolders),
|
||||
_ => Err(rquickjs::Error::FromJs {
|
||||
from: "string",
|
||||
to: "TemplateType",
|
||||
message: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for GeneratorFn {
|
||||
fn from_warp_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
if value.is_object() {
|
||||
let object = Object::from_value(value)?;
|
||||
let value: Value = object.get("script")?;
|
||||
let script = GeneratorScript::from_warp_js(ctx, value, js_function_registry)?;
|
||||
let post_process = if object.contains_key("postProcess")? {
|
||||
let function: Function = object.get("postProcess")?;
|
||||
let function_ref = js_function_registry
|
||||
.register_js_function::<String, GeneratorResults>(function, ctx);
|
||||
Some(function_ref)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(GeneratorFn::ShellCommand {
|
||||
script,
|
||||
post_process,
|
||||
})
|
||||
} else if value.is_function() {
|
||||
let function: Function = Function::from_value(value)?;
|
||||
let function_ref = js_function_registry
|
||||
.register_js_function::<GeneratorCompletionContext, GeneratorResults>(
|
||||
function, ctx,
|
||||
);
|
||||
Ok(GeneratorFn::Custom(function_ref))
|
||||
} else {
|
||||
Err(rquickjs::Error::FromJs {
|
||||
from: "generator_fn",
|
||||
to: "GeneratorFn",
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for GeneratorScript {
|
||||
fn from_warp_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
if value.is_string() {
|
||||
Ok(GeneratorScript::Static(String::from_js(ctx, value)?))
|
||||
} else if value.is_function() {
|
||||
let script_fn: Function = Function::from_value(value)?;
|
||||
let function_ref =
|
||||
js_function_registry.register_js_function::<Vec<String>, String>(script_fn, ctx);
|
||||
Ok(GeneratorScript::Dynamic(function_ref))
|
||||
} else {
|
||||
Err(rquickjs::Error::FromJs {
|
||||
from: "script",
|
||||
to: "GeneratorScript",
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for Opt {
|
||||
fn from_warp_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
let object = Object::from_value(value)?;
|
||||
let name: Vec<String> =
|
||||
get_one_or_more_required(&object, "name", js_function_registry, ctx)?;
|
||||
let description: Option<String> =
|
||||
get_optional(&object, "description", js_function_registry, ctx)?;
|
||||
let required: bool =
|
||||
get_optional(&object, "required", js_function_registry, ctx)?.unwrap_or(false);
|
||||
let arguments: Vec<Argument> =
|
||||
get_one_or_more_optional(&object, "arguments", js_function_registry, ctx)?;
|
||||
let priority: Option<i32> = get_optional(&object, "priority", js_function_registry, ctx)?;
|
||||
|
||||
Ok(Opt {
|
||||
name,
|
||||
description,
|
||||
arguments,
|
||||
required,
|
||||
priority: priority.map(Priority::new).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for GeneratorResults {
|
||||
fn from_warp_js(
|
||||
ctx: rquickjs::Ctx<'js>,
|
||||
value: rquickjs::Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self> {
|
||||
let object = Object::from_value(value)?;
|
||||
let suggestions =
|
||||
get_one_or_more_required(&object, "suggestions", js_function_registry, ctx)?;
|
||||
let is_ordered =
|
||||
get_optional(&object, "is_ordered", js_function_registry, ctx)?.unwrap_or(false);
|
||||
|
||||
Ok(GeneratorResults {
|
||||
suggestions,
|
||||
is_ordered,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> IntoWarpJs<'js> for GeneratorCompletionContext {
|
||||
fn into_warp_js(self, ctx: rquickjs::Ctx<'js>) -> rquickjs::Result<Value<'js>> {
|
||||
let object = Object::new(ctx)?;
|
||||
object.set("tokens", self.tokens)?;
|
||||
object.set("pwd", self.pwd)?;
|
||||
Ok(object.into_value())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//! This module contains functions for looking up a matching command signature from tokenized and
|
||||
//! untokenized input.
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::{registry::CommandRegistry, Command};
|
||||
|
||||
/// Returns the highest-precedence matching `Command` signature object for the given `input`, if
|
||||
/// any, along with the index of the token in `input` matched to the returned `Command`.
|
||||
///
|
||||
/// Subcommands take precedence over parent commands.
|
||||
///
|
||||
/// Note that a token in the input must have trailing whitespace (e.g. marking it as "completed")
|
||||
/// to be eligible to be matched to a command signature. So, for example, if the input does not
|
||||
/// contain trailing whitespace, the last token is not considered in the matching algorithm.
|
||||
/// Otherwise, if one subcommand is a prefix of another subcommand, we could mistakenly eagerly
|
||||
/// return the signature for the shorter subcommand even if the intent was to continue typing to
|
||||
/// enter the longer subcommand.
|
||||
///
|
||||
/// Practically, this means that for input "test_command test_subcommand", even if there is a
|
||||
/// subcommand signature for "test_subcommand", this returns the signature for "test_command",
|
||||
/// because it's assumed "test_subcommand" may still be edited.
|
||||
pub fn get_matching_signature_for_input<'a>(
|
||||
input: &str,
|
||||
registry: &'a CommandRegistry,
|
||||
) -> Option<(&'a Command, usize)> {
|
||||
let input_tokens = input.split_whitespace().collect_vec();
|
||||
get_matching_signature_for_tokenized_input(
|
||||
&input_tokens,
|
||||
input.ends_with(char::is_whitespace),
|
||||
registry,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the highest-precedence matching `Command` signature object for the given tokenized
|
||||
/// `input`, if any. This is equivalent to `get_matching_signature_input` above, except input is
|
||||
/// tokenized (e.g. given as an array of string tokens, which were assumed to be space-delimited in
|
||||
/// the original input). Because input is tokenized, the caller needs to explicitly specify whether
|
||||
/// the original input had trailing whitespace to determine if the last token is eligible for use
|
||||
/// in the matching algorithm.
|
||||
///
|
||||
/// See comments on `get_matching_signature_input` for more details.
|
||||
pub fn get_matching_signature_for_tokenized_input<'a>(
|
||||
input_tokens: &[&str],
|
||||
has_trailing_whitespace: bool,
|
||||
registry: &'a CommandRegistry,
|
||||
) -> Option<(&'a Command, usize)> {
|
||||
let (first_token, remaining_tokens) = input_tokens.split_first()?;
|
||||
|
||||
// Find the top level signature.
|
||||
registry.get_signature(first_token).map(|signature| {
|
||||
deepest_matching_subcommand_signature(
|
||||
remaining_tokens,
|
||||
&signature.command,
|
||||
0,
|
||||
has_trailing_whitespace,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Given a parent `command_signature`, resolves the most specific (deepest) subcommand that
|
||||
/// the user has entered in `input_tokens`, skipping over any flags that appear
|
||||
/// before the subcommand name (e.g. `kubectl -n kube-system get` resolves to `get`).
|
||||
///
|
||||
/// Returns the matched `Command` along with the index of its token in `input_tokens`.
|
||||
/// If no subcommand is found, `command_signature` itself is returned at `current_token_index`.
|
||||
///
|
||||
/// The last token is only eligible for a subcommand match when `has_trailing_whitespace` is
|
||||
/// true, i.e. the user has finished typing it.
|
||||
fn deepest_matching_subcommand_signature<'a>(
|
||||
input_tokens: &[&str],
|
||||
command_signature: &'a Command,
|
||||
mut current_token_index: usize,
|
||||
has_trailing_whitespace: bool,
|
||||
) -> (&'a Command, usize) {
|
||||
if input_tokens.is_empty() {
|
||||
return (command_signature, current_token_index);
|
||||
}
|
||||
|
||||
// Save the starting index before we begin scanning for subcommands.
|
||||
// If we skip past flags but never find a subcommand beyond them, we
|
||||
// return this index so that `parse_internal_command` treats the flags
|
||||
// as arguments to be parsed rather than swallowing them into the
|
||||
// command name.
|
||||
let subcommand_search_start_index = current_token_index;
|
||||
|
||||
while current_token_index < input_tokens.len() {
|
||||
let is_last_token = current_token_index == input_tokens.len() - 1;
|
||||
let token = input_tokens[current_token_index];
|
||||
|
||||
// Try to match the token against a subcommand.
|
||||
let subcommand_match = command_signature.subcommands.iter().find(|subcommand| {
|
||||
let token_matches_subcommand = token == subcommand.name.as_str();
|
||||
if is_last_token {
|
||||
// If this is the last token, treat the subcommand signature as a match
|
||||
// if there is trailing whitespace, which affirms the user's intent to use
|
||||
// that subcommand. If there is no trailing whitespace, the user may still
|
||||
// be in the process of editing that subcommand (or specifying a different
|
||||
// subcommand of which the current token is a prefix).
|
||||
token_matches_subcommand && has_trailing_whitespace
|
||||
} else {
|
||||
token_matches_subcommand
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(subcommand) = subcommand_match {
|
||||
return deepest_matching_subcommand_signature(
|
||||
input_tokens,
|
||||
subcommand,
|
||||
current_token_index + 1,
|
||||
has_trailing_whitespace,
|
||||
);
|
||||
}
|
||||
|
||||
// If the token is a flag (starts with '-'), try to skip past it and its arguments
|
||||
// to continue looking for subcommands. This handles cases like
|
||||
// `kubectl -n kube-system get pods` where flags appear before subcommands.
|
||||
if token.starts_with('-') {
|
||||
if let Some(option) = command_signature
|
||||
.options
|
||||
.iter()
|
||||
.find(|opt| opt.name.iter().any(|name| name == token))
|
||||
{
|
||||
// Skip the flag's arguments (non-switch options consume the next token(s)).
|
||||
// Clamp to the number of argument tokens actually present to avoid
|
||||
// advancing past the end of input_tokens (e.g. `kubectl -n ` with no
|
||||
// namespace value).
|
||||
let num_args = option.arguments.iter().filter(|arg| !arg.optional).count();
|
||||
let available = input_tokens.len().saturating_sub(current_token_index + 1);
|
||||
current_token_index += num_args.min(available);
|
||||
}
|
||||
// Advance past the flag token itself.
|
||||
current_token_index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Token is not a subcommand or a recognized flag; stop searching.
|
||||
break;
|
||||
}
|
||||
|
||||
// No subcommand was found beyond any skipped flags, so return the
|
||||
// start index. This ensures the caller's parser still sees those
|
||||
// flag tokens and can process them as flag arguments.
|
||||
(command_signature, subcommand_search_start_index)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lookup_test.rs"]
|
||||
mod test;
|
||||
@@ -0,0 +1,418 @@
|
||||
use crate::signatures::{Argument, Command, CommandSignature, Opt};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Creates a `test_command` signature with a `test_subcommand` subcommand
|
||||
/// and the given options on the root command.
|
||||
fn test_command_signature_with_options(options: Vec<Opt>) -> CommandSignature {
|
||||
CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![Command {
|
||||
name: "test_subcommand".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
options,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn valued_option() -> Opt {
|
||||
Opt {
|
||||
name: vec!["-n".to_owned(), "--name".to_owned()],
|
||||
arguments: vec![Argument {
|
||||
name: "value".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_on_root_command() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) = get_matching_signature_for_input("test_command ", ®istry)
|
||||
.expect("Signature should exist");
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_on_root_command_with_argument() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![Command {
|
||||
name: "test_subcommand".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
arguments: vec![Argument {
|
||||
name: "arg1".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) =
|
||||
get_matching_signature_for_input("test_command some_arg_value ", ®istry)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_on_subcommand() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![Command {
|
||||
name: "test_subcommand".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
arguments: vec![Argument {
|
||||
name: "arg1".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) =
|
||||
get_matching_signature_for_input("test_command test_subcommand ", ®istry)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(index, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_on_subcommand_with_argument() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![
|
||||
Command {
|
||||
name: "test_subcommand1".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "test_subcommand2".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
arguments: vec![Argument {
|
||||
name: "test_command_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) = get_matching_signature_for_input(
|
||||
"test_command test_subcommand1 some_arg_value ",
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand1");
|
||||
assert_eq!(index, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_without_trailing_whitespace() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![Command {
|
||||
name: "test_subcommand".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
arguments: vec![Argument {
|
||||
name: "arg1".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) =
|
||||
get_matching_signature_for_input("test_command test_subcommand", ®istry)
|
||||
.expect("Signature should be found.");
|
||||
|
||||
// The matched signature should be that of the top-level command. Because there is no trailing
|
||||
// whitespace in the input, it's assumed we're still completing on the "test_subcommand", so we
|
||||
// should still be using the top-level command signature.
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_tokenized_input() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![
|
||||
Command {
|
||||
name: "test_subcommand1".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "test_subcommand2".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
arguments: vec![Argument {
|
||||
name: "test_command_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "test_subcommand1", "some_arg_value"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand1");
|
||||
assert_eq!(token_index, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_tokenized_input_without_trailing_whitespace() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![
|
||||
Command {
|
||||
name: "test_subcommand1".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "test_subcommand2".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
arguments: vec![Argument {
|
||||
name: "test_command_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "test_subcommand1"],
|
||||
false,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
|
||||
// The matched signature should be that of the top-level command. Because there is no trailing
|
||||
// whitespace in the input, it's assumed we're still completing on the "test_subcommand", so we
|
||||
// should still be using the top-level command signature.
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_skips_flag_with_value_before_subcommand() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![valued_option()]));
|
||||
|
||||
// -n takes a value, so the parser should skip "-n val" and find test_subcommand.
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "-n", "val", "test_subcommand"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_skips_long_flag_with_value_before_subcommand() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![
|
||||
Opt {
|
||||
name: vec!["--context".to_owned()],
|
||||
arguments: vec![Argument {
|
||||
name: "context".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
valued_option(),
|
||||
]));
|
||||
|
||||
// Two valued flags before the subcommand should both be skipped.
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&[
|
||||
"test_command",
|
||||
"--context",
|
||||
"staging",
|
||||
"-n",
|
||||
"project1",
|
||||
"test_subcommand",
|
||||
],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_skips_switch_flag_before_subcommand() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![Opt {
|
||||
name: vec!["--verbose".to_owned()],
|
||||
arguments: vec![],
|
||||
..Default::default()
|
||||
}]));
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "--verbose", "test_subcommand"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_flag_at_end_without_value_does_not_panic() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![valued_option()]));
|
||||
|
||||
// "-n" with no value should not panic.
|
||||
let (found_signature, token_index) =
|
||||
get_matching_signature_for_tokenized_input(&["test_command", "-n"], true, ®istry)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
// No subcommand found, so entry_token_index (0) is returned.
|
||||
assert_eq!(token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_skips_unrecognized_flag_before_subcommand() {
|
||||
// Unrecognized flags (tokens starting with '-' not in the spec) should be
|
||||
// skipped so the parser can still discover subcommands after them.
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![]));
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "--unknown-flag", "test_subcommand"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_flag_arg_consumes_token_matching_subcommand_name() {
|
||||
// When a recognized flag takes a required argument, the next token is
|
||||
// consumed as the flag's value even if it matches a subcommand name.
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![valued_option()]));
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "-n", "test_subcommand", "extra"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
// "test_subcommand" was consumed as -n's value, so no subcommand is found.
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_only_flags_no_subcommand() {
|
||||
// When the input consists only of flags with no following subcommand,
|
||||
// the parent command should be returned at the entry index.
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![valued_option()]));
|
||||
|
||||
let (found_signature, token_index) =
|
||||
get_matching_signature_for_tokenized_input(&["test_command", "-n", "val"], true, ®istry)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_optional_flag_arg_does_not_consume_subcommand() {
|
||||
// A flag with 1 required + 1 optional argument should only skip the
|
||||
// required arg, so the next token can still match a subcommand.
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![Opt {
|
||||
name: vec!["--output".to_owned()],
|
||||
arguments: vec![
|
||||
Argument {
|
||||
name: "format".to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
Argument {
|
||||
name: "extra".to_owned(),
|
||||
optional: true,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}]));
|
||||
|
||||
// "json" is the required arg, "test_subcommand" should not be consumed as
|
||||
// the optional arg.
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "--output", "json", "test_subcommand"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 3);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
//! This module contains Command Signature types for use with the v2 (JS-compatible) completions
|
||||
//! engine.
|
||||
|
||||
// The `js` module contains implementations of `warp_js::{IntoWarpJs, FromWarpJs}` for V2 command
|
||||
// signatures, which is only supported on native non-wasm platforms.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod js;
|
||||
|
||||
mod lookup;
|
||||
mod registry;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
pub use lookup::*;
|
||||
pub use registry::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use warp_js::TypedJsFunctionRef;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "test-util", derive(Default))]
|
||||
pub struct CommandSignature {
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "test-util", derive(Default))]
|
||||
pub struct Command {
|
||||
pub name: String,
|
||||
pub alias: Vec<String>,
|
||||
pub description: Option<String>,
|
||||
pub arguments: Vec<Argument>,
|
||||
pub subcommands: Vec<Command>,
|
||||
pub options: Vec<Opt>,
|
||||
pub priority: Priority,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "test-util", derive(Default))]
|
||||
pub struct Argument {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub values: Vec<ArgumentValue>,
|
||||
pub optional: bool,
|
||||
pub arity: Option<Arity>,
|
||||
}
|
||||
|
||||
impl Argument {
|
||||
pub fn is_variadic(&self) -> bool {
|
||||
self.arity
|
||||
.as_ref()
|
||||
.is_some_and(|arity| arity.limit.is_none())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "test-util", derive(Default))]
|
||||
pub struct Arity {
|
||||
pub limit: Option<usize>,
|
||||
pub delimiter: Option<ArgumentDelimiter>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ArgumentDelimiter(pub String);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ArgumentValue {
|
||||
Suggestion(Suggestion),
|
||||
Template {
|
||||
type_name: TemplateType,
|
||||
filter_name: Option<String>,
|
||||
},
|
||||
Generator(GeneratorFn),
|
||||
/// The argument itself is a root command.
|
||||
///
|
||||
/// This is the appropriate `ArgumentValue` for commands that take a full command as an
|
||||
/// argument: `time` or `sudo`, for example.
|
||||
RootCommand,
|
||||
}
|
||||
|
||||
/// The final set of results returned from a custom `GeneratorFn` or from a `GeneratorFn`'s
|
||||
/// `post_process` function.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeneratorResults {
|
||||
/// The list of completion suggestions.
|
||||
pub suggestions: Vec<Suggestion>,
|
||||
|
||||
/// `true` if the order of `suggestions` should be preserved.
|
||||
///
|
||||
/// If `false`, `suggestions` may be re-ordered by the internal completions engine in the final
|
||||
/// result set.
|
||||
pub is_ordered: bool,
|
||||
}
|
||||
|
||||
/// The input struct passed to custom generator functions.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeneratorCompletionContext {
|
||||
/// The tokens in the input for which completion suggestions are being generated.
|
||||
pub tokens: Vec<String>,
|
||||
|
||||
/// The current working directory of the session.
|
||||
pub pwd: String,
|
||||
}
|
||||
|
||||
/// The Rust representation of a JS Function used to generate argument value suggestions.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum GeneratorFn {
|
||||
/// A generator that computes suggestions by executing the given `script` (a sh command) and
|
||||
/// "post-processing" its stdout with the given `post_process` fn.
|
||||
ShellCommand {
|
||||
script: GeneratorScript,
|
||||
|
||||
/// If `None`, a "default" post process function implementation is used, where
|
||||
/// `Suggestion`s are created from each line in `script`'s stdout. The returned
|
||||
/// `GeneratorResults` object's `is_ordered` is set to `false`.
|
||||
post_process: Option<TypedJsFunctionRef<String, GeneratorResults>>,
|
||||
},
|
||||
/// An entirely user-specified JS function that generates suggestions based on the given
|
||||
/// `GeneratorCompletionContext`.
|
||||
Custom(TypedJsFunctionRef<GeneratorCompletionContext, GeneratorResults>),
|
||||
}
|
||||
|
||||
/// The command to be executed as part of a `GeneratorFn::ShellCommand` to generate argument
|
||||
/// suggestion values.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum GeneratorScript {
|
||||
Static(String),
|
||||
/// A JS function that dynamically computes the command to be run based on the tokenized input
|
||||
/// for which suggestions are being generated.
|
||||
Dynamic(TypedJsFunctionRef<Vec<String>, String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "test-util", derive(Default))]
|
||||
pub struct Suggestion {
|
||||
pub value: String,
|
||||
pub display_value: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub priority: Priority,
|
||||
}
|
||||
|
||||
/// The lowest priority value a completion object can have.
|
||||
const MIN_PRIORITY: i32 = -100;
|
||||
|
||||
/// The priority value of a completion object if not otherwise specifiied.
|
||||
const DEFAULT_PRIORITY: i32 = 0;
|
||||
|
||||
/// The highest priority value a completion object can have.
|
||||
const MAX_PRIORITY: i32 = 100;
|
||||
|
||||
/// Priority is a property of Commands, Subcommands and Options that influences where in the
|
||||
/// suggestion list those objects appear. It is represented as an integer between -100 and 100
|
||||
/// (inclusive) with 0 as the default.
|
||||
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Copy, Clone)]
|
||||
pub struct Priority(i32);
|
||||
|
||||
impl Priority {
|
||||
/// Creates a Priority value clamped to the range [-100, 100].
|
||||
pub fn new(value: i32) -> Self {
|
||||
Self(value.clamp(MIN_PRIORITY, MAX_PRIORITY))
|
||||
}
|
||||
|
||||
pub fn value(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn min() -> Self {
|
||||
Self::new(MIN_PRIORITY)
|
||||
}
|
||||
|
||||
pub fn max() -> Self {
|
||||
Self::new(MAX_PRIORITY)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Priority {
|
||||
fn default() -> Self {
|
||||
Self::new(DEFAULT_PRIORITY)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Priority {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.0.cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Priority {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum TemplateType {
|
||||
Files,
|
||||
Folders,
|
||||
FilesAndFolders,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "test-util", derive(Default))]
|
||||
pub struct Opt {
|
||||
pub name: Vec<String>,
|
||||
pub description: Option<String>,
|
||||
pub arguments: Vec<Argument>,
|
||||
pub required: bool,
|
||||
pub priority: Priority,
|
||||
}
|
||||
|
||||
impl Opt {
|
||||
/// Returns `true` if this `Opt` has the given name.
|
||||
///
|
||||
/// Note that the given `name` should not include any leading hyphens; for example, this
|
||||
/// returns true for an `Opt` with names ['-f', '--foo'] given name 'f' or 'foo'.
|
||||
pub fn has_name(&self, name: impl AsRef<str>) -> bool {
|
||||
self.name.iter().any(|option_name| {
|
||||
if let Some(rest) = option_name.strip_prefix("--") {
|
||||
rest == name.as_ref()
|
||||
} else if let Some(rest) = option_name.strip_prefix('-') {
|
||||
rest == name.as_ref()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "signatures_test.rs"]
|
||||
mod test;
|
||||
@@ -0,0 +1,55 @@
|
||||
//! `CommandRegistry` contains all registered `CommandSignature`s that are eligible for use in
|
||||
//! completion suggestion generation.
|
||||
//!
|
||||
//! Completion engine callers must supply a `CommandRegistry` in their `CompletionContext`
|
||||
//! implementation to generate suggestions for registered commands.
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use memo_map::MemoMap;
|
||||
|
||||
use super::CommandSignature;
|
||||
|
||||
static GLOBAL_REGISTRY: OnceLock<Arc<CommandRegistry>> = OnceLock::new();
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct CommandRegistry {
|
||||
signatures: MemoMap<String, CommandSignature>,
|
||||
}
|
||||
|
||||
impl CommandRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
signatures: MemoMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_signature(&self, command: impl AsRef<str>) -> Option<&CommandSignature> {
|
||||
self.signatures.get(command.as_ref())
|
||||
}
|
||||
|
||||
pub fn register_signature(&self, signature: CommandSignature) {
|
||||
self.signatures
|
||||
.insert(signature.command.name.clone(), signature);
|
||||
}
|
||||
|
||||
pub fn registered_commands(&self) -> impl Iterator<Item = &str> {
|
||||
self.signatures.keys().map(|key| key.as_str())
|
||||
}
|
||||
|
||||
pub fn global_instance() -> Arc<Self> {
|
||||
GLOBAL_REGISTRY
|
||||
.get_or_init(|| {
|
||||
// TODO(wasm): Determine how to asynchronously load command signatures on wasm.
|
||||
Arc::new(CommandRegistry::new())
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "registry_test.rs"]
|
||||
mod test;
|
||||
@@ -0,0 +1,28 @@
|
||||
use crate::signatures::{Command, Priority};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_signature() -> CommandSignature {
|
||||
CommandSignature {
|
||||
command: Command {
|
||||
name: "test".to_owned(),
|
||||
alias: vec![],
|
||||
description: None,
|
||||
arguments: vec![],
|
||||
subcommands: vec![],
|
||||
options: vec![],
|
||||
priority: Priority::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_registry_registers_signature() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_signature());
|
||||
|
||||
let signature = registry
|
||||
.get_signature("test")
|
||||
.expect("Signature is registered.");
|
||||
assert_eq!(signature.command.name, "test");
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use super::Priority;
|
||||
|
||||
#[test]
|
||||
fn test_priority_normalization() {
|
||||
let too_small = Priority::new(-201);
|
||||
assert_eq!(Priority::min(), too_small);
|
||||
|
||||
let too_large = Priority::new(201);
|
||||
assert_eq!(Priority::max(), too_large);
|
||||
|
||||
let fourty_two = Priority::new(42);
|
||||
assert_eq!(42, fourty_two.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_priority_comparison() {
|
||||
let super_important = Priority::new(200);
|
||||
let important = Priority::new(40);
|
||||
let not_important = Priority::new(-80);
|
||||
|
||||
assert!(super_important == super_important);
|
||||
assert!(super_important > important);
|
||||
assert!(super_important > not_important);
|
||||
|
||||
assert!(important == important);
|
||||
assert!(important > not_important);
|
||||
|
||||
assert!(not_important == not_important);
|
||||
}
|
||||
Reference in New Issue
Block a user