Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
@@ -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(&registry);
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);
}