2300 lines
73 KiB
Rust
2300 lines
73 KiB
Rust
use std::collections::HashMap;
|
|
use std::iter::FromIterator;
|
|
|
|
use typed_path::TypedPathBuf;
|
|
|
|
use crate::completer::context::CompletionContext;
|
|
use crate::completer::engine::EngineDirEntry;
|
|
use crate::completer::matchers::MatchStrategy;
|
|
use crate::completer::testing::{
|
|
FakeCompletionContext, MockGeneratorContext, MockPathCompletionContext,
|
|
};
|
|
use crate::meta::Span;
|
|
use crate::signatures::testing::{
|
|
cd_signature, create_test_command_registry, fuzzy_signature, git_signature, java_signature,
|
|
ls_signature, npm_signature, signature_with_empty_positional, test_signature,
|
|
};
|
|
use crate::signatures::CommandRegistry;
|
|
|
|
use super::CompleterOptions;
|
|
use super::{suggestions, CompletionsFallbackStrategy, SuggestionResults, SuggestionType};
|
|
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(not(feature = "v2"))] {
|
|
use std::collections::HashSet;
|
|
use crate::signatures::testing::add_content_signature;
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
mod windows_constants {
|
|
pub(super) const TEST_WORK_DIR: &str = r"C:\Users\";
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
use windows_constants::*;
|
|
|
|
#[cfg(unix)]
|
|
mod unix_constants {
|
|
pub(super) const TEST_WORK_DIR: &str = "/home/";
|
|
#[allow(dead_code)]
|
|
pub(super) const TEST_ROOT_DIR: &str = "/";
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
use unix_constants::*;
|
|
|
|
/// Same API as `suggestions` but not async because this is a test :)
|
|
fn suggestions_for_test<T: CompletionContext>(
|
|
line: &str,
|
|
pos: usize,
|
|
options: CompleterOptions,
|
|
ctx: &T,
|
|
) -> Option<SuggestionResults> {
|
|
galaxyui::r#async::block_on(suggestions(line, pos, None, options, ctx))
|
|
}
|
|
|
|
/// Runs the completer at the end of the given line and returns the associated
|
|
/// suggestions, NOT including SuggestionType::Option (flags) by default. If
|
|
/// you want to include flags in the result, use complete_at_end_of_line_with_options
|
|
fn complete_at_end_of_line<T: CompletionContext>(line: &str, ctx: &T) -> Vec<String> {
|
|
suggestions_for_test(
|
|
line,
|
|
line.len(),
|
|
CompleterOptions {
|
|
match_strategy: MatchStrategy::CaseInsensitive,
|
|
fallback_strategy: CompletionsFallbackStrategy::FilePaths,
|
|
suggest_file_path_completions_only: false,
|
|
parse_quotes_as_literals: false,
|
|
},
|
|
ctx,
|
|
)
|
|
.into_iter()
|
|
.flat_map(|res| res.suggestions)
|
|
.filter_map(|s| match s.suggestion_type() {
|
|
SuggestionType::Option(..) => None,
|
|
_ => Some(s.suggestion.display.to_string()),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Runs the completer at the end of the given line with suggest_file_path_completions_only true,
|
|
/// and returns the associated suggestions, NOT including SuggestionType::Option (flags) by default.
|
|
fn complete_at_end_of_line_file_path_only<T: CompletionContext>(
|
|
line: &str,
|
|
ctx: &T,
|
|
) -> Vec<String> {
|
|
suggestions_for_test(
|
|
line,
|
|
line.len(),
|
|
CompleterOptions {
|
|
match_strategy: MatchStrategy::CaseInsensitive,
|
|
fallback_strategy: CompletionsFallbackStrategy::FilePaths,
|
|
suggest_file_path_completions_only: true,
|
|
parse_quotes_as_literals: false,
|
|
},
|
|
ctx,
|
|
)
|
|
.into_iter()
|
|
.flat_map(|res| res.suggestions)
|
|
.filter_map(|s| match s.suggestion_type() {
|
|
SuggestionType::Option(..) => None,
|
|
_ => Some(s.suggestion.display.to_string()),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
// Runs the completer and then uses the query to filter down the results.
|
|
// Note that we need this as a helper test function to test some specific
|
|
// ordering/matching logic -- ideally this will eventually be baked into
|
|
// `suggestions` itself eventually.
|
|
fn complete_at_end_of_line_with_query<T: CompletionContext>(
|
|
line: &str,
|
|
query: &str,
|
|
match_strategy: MatchStrategy,
|
|
ctx: &T,
|
|
) -> Vec<String> {
|
|
suggestions_for_test(
|
|
line,
|
|
line.len(),
|
|
CompleterOptions {
|
|
match_strategy,
|
|
fallback_strategy: CompletionsFallbackStrategy::FilePaths,
|
|
suggest_file_path_completions_only: false,
|
|
parse_quotes_as_literals: false,
|
|
},
|
|
ctx,
|
|
)
|
|
.expect("suggestion results should be some")
|
|
.filter_by_query(query, &['/'])
|
|
.map(|s| s.suggestion.display.to_string())
|
|
.collect()
|
|
}
|
|
|
|
/// Returns a vector of tuples each of which consists of the display string
|
|
/// for a matched suggestion and the matched char indices of that string
|
|
fn get_filtered_suggestions_with_query<'a, T: CompletionContext>(
|
|
line: &'a str,
|
|
query: &'a str,
|
|
match_strategy: MatchStrategy,
|
|
ctx: &'a T,
|
|
) -> Vec<(Vec<usize>, String)> {
|
|
suggestions_for_test(
|
|
line,
|
|
line.len(),
|
|
CompleterOptions {
|
|
match_strategy,
|
|
fallback_strategy: CompletionsFallbackStrategy::FilePaths,
|
|
suggest_file_path_completions_only: false,
|
|
parse_quotes_as_literals: false,
|
|
},
|
|
ctx,
|
|
)
|
|
.expect("suggestion results should be some")
|
|
.filter_by_query(query, &['/'])
|
|
.map(|s| (s.matching_indices, s.suggestion.display.to_string()))
|
|
.collect()
|
|
}
|
|
|
|
fn complete_at_end_of_line_with_options<T: CompletionContext>(
|
|
line: &str,
|
|
match_strategy: MatchStrategy,
|
|
ctx: &T,
|
|
) -> Vec<String> {
|
|
suggestions_for_test(
|
|
line,
|
|
line.len(),
|
|
CompleterOptions {
|
|
match_strategy,
|
|
fallback_strategy: CompletionsFallbackStrategy::FilePaths,
|
|
suggest_file_path_completions_only: false,
|
|
parse_quotes_as_literals: false,
|
|
},
|
|
ctx,
|
|
)
|
|
.into_iter()
|
|
.flat_map(|res| res.suggestions)
|
|
.map(|s| s.suggestion.display.to_string())
|
|
.collect()
|
|
}
|
|
|
|
fn complete_at_cursor_position<T: CompletionContext>(
|
|
line: &str,
|
|
pos: usize,
|
|
ctx: &T,
|
|
) -> Vec<String> {
|
|
suggestions_for_test(
|
|
line,
|
|
pos,
|
|
CompleterOptions {
|
|
match_strategy: MatchStrategy::CaseInsensitive,
|
|
fallback_strategy: CompletionsFallbackStrategy::FilePaths,
|
|
suggest_file_path_completions_only: false,
|
|
parse_quotes_as_literals: false,
|
|
},
|
|
ctx,
|
|
)
|
|
.into_iter()
|
|
.flat_map(|res| res.suggestions)
|
|
.map(|s| s.suggestion.display.to_string())
|
|
.collect()
|
|
}
|
|
|
|
fn complete_replacement_span<T: CompletionContext>(line: &str, ctx: &T) -> Option<Span> {
|
|
suggestions_for_test(
|
|
line,
|
|
line.len(),
|
|
CompleterOptions {
|
|
match_strategy: MatchStrategy::CaseInsensitive,
|
|
fallback_strategy: CompletionsFallbackStrategy::FilePaths,
|
|
suggest_file_path_completions_only: false,
|
|
parse_quotes_as_literals: false,
|
|
},
|
|
ctx,
|
|
)
|
|
.map(|results| results.replacement_span)
|
|
}
|
|
|
|
fn complete_replacement_span_at_cursor_pos<T: CompletionContext>(
|
|
line: &str,
|
|
pos: usize,
|
|
ctx: &T,
|
|
) -> Option<Span> {
|
|
suggestions_for_test(
|
|
line,
|
|
pos,
|
|
CompleterOptions {
|
|
match_strategy: MatchStrategy::CaseInsensitive,
|
|
fallback_strategy: CompletionsFallbackStrategy::FilePaths,
|
|
suggest_file_path_completions_only: false,
|
|
parse_quotes_as_literals: false,
|
|
},
|
|
ctx,
|
|
)
|
|
.map(|results| results.replacement_span)
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_top_level_command_completions() {
|
|
let registry = create_test_command_registry([]);
|
|
let ctx = FakeCompletionContext::new(registry).with_top_level_commands(["git", "cd", "cargo"]);
|
|
|
|
assert_eq!(complete_at_end_of_line("c", &ctx), vec!["cargo", "cd"]);
|
|
assert_eq!(complete_at_end_of_line("g", &ctx), vec!["git"]);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_command_completions_requires_nonwhitespace() {
|
|
let registry = create_test_command_registry([]);
|
|
let ctx = FakeCompletionContext::new(registry).with_top_level_commands(["git", "cd", "cargo"]);
|
|
|
|
// Empty strings produce no completions.
|
|
assert!(complete_at_end_of_line("", &ctx).is_empty());
|
|
// Whitespace (without any other characters) produces no completions.
|
|
assert!(complete_at_end_of_line("\t\n\t", &ctx).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_files_includes_directories() {
|
|
let path_ctx = MockPathCompletionContext::default().with_entries_in_pwd([
|
|
EngineDirEntry::test_dir("foo"),
|
|
EngineDirEntry::test_file("foobar"),
|
|
]);
|
|
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
|
.with_path_completion_context(path_ctx);
|
|
|
|
// Even though cat only accepts file, we also suggest directories since the user could
|
|
// be trying to cat a nested file within the directory.
|
|
assert_eq!(
|
|
complete_at_end_of_line("cat ", &ctx),
|
|
vec!["foo/", "foobar"],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_completes_paths_with_space() {
|
|
let registry = create_test_command_registry([test_signature(), cd_signature()]);
|
|
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_entries_in_pwd([EngineDirEntry::test_dir("foo bar bazz")])
|
|
.with_entries(
|
|
pwd.join("foo bar bazz/"),
|
|
[EngineDirEntry::test_dir("test")],
|
|
);
|
|
|
|
let ctx = FakeCompletionContext::new(registry)
|
|
.with_supports_autocd(true)
|
|
.with_top_level_commands(["cd", "test"])
|
|
.with_path_completion_context(path_ctx);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line(r"cd foo\ bar\ bazz/", &ctx),
|
|
vec!["test/"],
|
|
);
|
|
|
|
// Path completions for top-level commands should also properly support directories with
|
|
// spaces.
|
|
assert_eq!(
|
|
complete_at_end_of_line(r"foo\ bar\ bazz/", &ctx),
|
|
vec!["test/"],
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line(r"./foo\ bar\ bazz/", &ctx),
|
|
vec!["test/"],
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --template-args-for-opt foo\\ bar\\ bazz/", &ctx),
|
|
vec!["test/"],
|
|
);
|
|
}
|
|
|
|
#[cfg_attr(
|
|
windows,
|
|
ignore = "CORE-3696: path sorting comparison function needs separators"
|
|
)]
|
|
#[test]
|
|
pub fn test_completes_dotfiles() {
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_file(".foo"),
|
|
EngineDirEntry::test_file("foobar"),
|
|
EngineDirEntry::test_dir("foo"),
|
|
EngineDirEntry::test_dir(".bar"),
|
|
])
|
|
.with_entries(
|
|
pwd.join("foo/"),
|
|
[
|
|
EngineDirEntry::test_file(".hidden"),
|
|
EngineDirEntry::test_dir("src"),
|
|
],
|
|
)
|
|
.with_entries(pwd.join("foo/src/"), [EngineDirEntry::test_file(".hidden")]);
|
|
|
|
let ctx = FakeCompletionContext::new(create_test_command_registry([
|
|
cd_signature(),
|
|
test_signature(),
|
|
]))
|
|
.with_supports_autocd(true)
|
|
.with_top_level_commands(["cd", "test"])
|
|
.with_path_completion_context(path_ctx);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_query("cd ", "", MatchStrategy::CaseInsensitive, &ctx),
|
|
vec!["foo/"],
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("cd .", &ctx),
|
|
vec!["./", "../", ".bar/"],
|
|
);
|
|
|
|
// Dotfiles should not be included if the the path does not start with a dot.
|
|
assert_eq!(
|
|
complete_at_end_of_line("cat ", &ctx),
|
|
vec!["foo/", "foobar"],
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("cat .", &ctx),
|
|
vec!["./", "../", ".bar/", ".foo"],
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("cat f", &ctx),
|
|
vec!["foo/", "foobar"]
|
|
);
|
|
assert_eq!(complete_at_end_of_line("cat foo/", &ctx), vec!["src/"]);
|
|
assert_eq!(
|
|
complete_at_end_of_line("cat foo/.", &ctx),
|
|
vec!["./", "../", ".hidden"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("cat foo/src/.", &ctx),
|
|
vec!["./", "../", ".hidden"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_cursor_in_middle_of_line() {
|
|
let path_ctx = MockPathCompletionContext::new(TypedPathBuf::from(TEST_WORK_DIR))
|
|
.with_entries_in_pwd([EngineDirEntry::test_dir("foo")]);
|
|
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
|
.with_top_level_commands(["cargo", "cd"])
|
|
.with_path_completion_context(path_ctx);
|
|
|
|
let suggestions: Vec<String> = suggestions_for_test(
|
|
"cd ~/",
|
|
3,
|
|
CompleterOptions {
|
|
match_strategy: MatchStrategy::CaseInsensitive,
|
|
fallback_strategy: CompletionsFallbackStrategy::FilePaths,
|
|
suggest_file_path_completions_only: false,
|
|
parse_quotes_as_literals: false,
|
|
},
|
|
&ctx,
|
|
)
|
|
.expect("suggestions should be some")
|
|
.filter_by_query("", &['/'])
|
|
.map(|s| s.suggestion.display.to_string())
|
|
.collect();
|
|
assert_eq!(suggestions, vec!["foo/"]);
|
|
assert_eq!(
|
|
complete_replacement_span_at_cursor_pos("cd ~/", 3, &ctx),
|
|
Some(Span::new(3, 3))
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_cursor_position("cd ~/", 1, &ctx),
|
|
vec!["cargo", "cd"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_replacement_span_at_cursor_pos("cd ~/", 1, &ctx),
|
|
Some(Span::new(0, 1))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_cd_from_home_dir() {
|
|
let home_directory = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let pwd = home_directory.join("foo/");
|
|
let path_ctx = MockPathCompletionContext::new(pwd)
|
|
.with_home_directory(home_directory.to_string_lossy().to_string())
|
|
.with_entries(
|
|
home_directory,
|
|
[
|
|
EngineDirEntry::test_file("bar"),
|
|
EngineDirEntry::test_dir("foo"),
|
|
EngineDirEntry::test_dir("baz"),
|
|
],
|
|
);
|
|
let ctx = FakeCompletionContext::new(create_test_command_registry([cd_signature()]))
|
|
.with_path_completion_context(path_ctx);
|
|
|
|
assert_eq!(complete_at_end_of_line("cd ~/", &ctx), vec!["baz/", "foo/"]);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_completions_ordering_within_group() {
|
|
let mut ctx = FakeCompletionContext::new(CommandRegistry::default());
|
|
|
|
// TODO(completions-v2): Re-enable when embedded signatures are implemented. In the long-term we
|
|
// should create a test signature for `chmod` instead of using real command signatures.
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(not(feature = "v2"))] {
|
|
let chmod_res = complete_at_end_of_line("chmod ", &ctx);
|
|
assert_eq!(chmod_res, vec!["664", "744", "777", "a+rx", "u+x"],);
|
|
}
|
|
}
|
|
|
|
let path_ctx = MockPathCompletionContext::new(TypedPathBuf::from(TEST_WORK_DIR))
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_file("foo"),
|
|
EngineDirEntry::test_file("foobar"),
|
|
EngineDirEntry::test_dir("foobaz"),
|
|
]);
|
|
ctx = ctx.with_path_completion_context(path_ctx);
|
|
|
|
let vim_res = complete_at_end_of_line("vim ", &ctx);
|
|
assert_eq!(vim_res, vec!["foo", "foobar", "foobaz/"],);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_completions_ordering_across_groups() {
|
|
let registry = create_test_command_registry([git_signature()]);
|
|
|
|
let path_ctx = MockPathCompletionContext::new(TypedPathBuf::from(TEST_WORK_DIR))
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_dir("gids"),
|
|
EngineDirEntry::test_dir("giants"),
|
|
]);
|
|
let ctx = FakeCompletionContext::new(registry)
|
|
.with_path_completion_context(path_ctx)
|
|
.with_supports_autocd(true)
|
|
.with_top_level_commands(vec!["gits", "git"]);
|
|
|
|
let gi_prefix_results = complete_at_end_of_line("gi", &ctx);
|
|
|
|
// commands before arguments/file paths (and each group should be sorted itself)
|
|
assert_eq!(gi_prefix_results, vec!["git", "gits", "giants/", "gids/"],);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_file_path_completions_only() {
|
|
let registry = create_test_command_registry([git_signature()]);
|
|
|
|
let path_ctx = MockPathCompletionContext::new(TypedPathBuf::from(TEST_WORK_DIR))
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_dir("gids"),
|
|
EngineDirEntry::test_dir("giants"),
|
|
]);
|
|
let ctx = FakeCompletionContext::new(registry)
|
|
.with_path_completion_context(path_ctx)
|
|
.with_supports_autocd(false)
|
|
.with_top_level_commands(vec!["gits", "git"]);
|
|
|
|
let gi_prefix_results = complete_at_end_of_line_file_path_only("gi", &ctx);
|
|
|
|
// Only file paths should exist, no commands
|
|
assert_eq!(gi_prefix_results, vec!["giants/", "gids/"]);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_file_path_completions_only_trailing_whitespace() {
|
|
let registry = create_test_command_registry([git_signature()]);
|
|
|
|
let path_ctx = MockPathCompletionContext::new(TypedPathBuf::from(TEST_WORK_DIR))
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_dir("gids"),
|
|
EngineDirEntry::test_dir("giants"),
|
|
]);
|
|
let ctx = FakeCompletionContext::new(registry)
|
|
.with_path_completion_context(path_ctx)
|
|
.with_supports_autocd(false)
|
|
.with_top_level_commands(vec!["gits", "git"]);
|
|
|
|
let gi_prefix_results = complete_at_end_of_line_file_path_only("find me files in ", &ctx);
|
|
|
|
// Only file paths should exist, no commands
|
|
assert_eq!(gi_prefix_results, vec!["giants/", "gids/"]);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_named_flags() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
// ensure all the named flags are appearing (ordering doesn't matter here)
|
|
let mut results =
|
|
complete_at_end_of_line_with_options("test --", MatchStrategy::CaseInsensitive, &ctx);
|
|
results.sort();
|
|
assert_eq!(
|
|
results,
|
|
vec![
|
|
"--long",
|
|
"--not-long",
|
|
"--required-and-optional-args",
|
|
"--required-args",
|
|
"--required-args-with-var",
|
|
"--template-args-for-opt"
|
|
]
|
|
);
|
|
|
|
assert!(complete_at_end_of_line("test --long", &ctx).is_empty());
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --long ", &ctx),
|
|
vec!["long-one", "long-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --long long", &ctx),
|
|
vec!["long-one", "long-two"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_variadic_flags() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
// ensure all the named flags are appearing (ordering doesn't matter here)
|
|
let mut results =
|
|
complete_at_end_of_line_with_options("test --", MatchStrategy::CaseInsensitive, &ctx);
|
|
results.sort();
|
|
assert_eq!(
|
|
results,
|
|
vec![
|
|
"--long",
|
|
"--not-long",
|
|
"--required-and-optional-args",
|
|
"--required-args",
|
|
"--required-args-with-var",
|
|
"--template-args-for-opt"
|
|
]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --long ", &ctx),
|
|
vec!["long-one", "long-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --long long-one --long ", &ctx),
|
|
vec!["long-one", "long-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --long long-one --not-long ", &ctx),
|
|
vec!["not-long-one", "not-long-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --long long-one ", &ctx),
|
|
vec!["long-one", "long-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --not-long not-long-one --long ", &ctx),
|
|
vec!["long-one", "long-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --not-long not-long-one --long long-one ", &ctx),
|
|
vec!["long-one", "long-two"]
|
|
);
|
|
}
|
|
|
|
// Tests that commands with multiple positional args complete with the correct results.
|
|
#[test]
|
|
pub fn test_positional_args() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test one ", &ctx),
|
|
vec!["one-one", "one-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test one one", &ctx),
|
|
vec!["one-one", "one-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test one one-one ", &ctx),
|
|
vec!["two-one", "two-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test one one-one two", &ctx),
|
|
vec!["two-one", "two-two"]
|
|
);
|
|
|
|
// The "three" subcommand has the positionals. The first and second are required while
|
|
// the third is optional.
|
|
assert_eq!(
|
|
complete_at_end_of_line("test three three-one three-two ", &ctx),
|
|
vec!["three-three"]
|
|
);
|
|
|
|
// Completions should still show even if part of the argument is already present.
|
|
assert_eq!(
|
|
complete_at_end_of_line("test three three-one three-two three-", &ctx),
|
|
vec!["three-three"]
|
|
);
|
|
|
|
assert!(
|
|
complete_at_end_of_line("test three three-one three-two three-three ", &ctx).is_empty()
|
|
);
|
|
}
|
|
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
pub fn test_command_alias() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
|
|
let generator_ctx = MockGeneratorContext::for_test_signature();
|
|
let ctx = FakeCompletionContext::new(registry).with_generator_context(generator_ctx);
|
|
|
|
// The test signature has an alias function, which expands subcommand "twelve" to "one".
|
|
assert_eq!(
|
|
complete_at_end_of_line("test twelve ", &ctx),
|
|
vec!["one-one", "one-two"]
|
|
);
|
|
|
|
// The test signature has an alias function, which expands subcommand "nine" to "twelve".
|
|
assert_eq!(
|
|
complete_at_end_of_line("test nine ", &ctx),
|
|
vec!["one-one", "one-two"]
|
|
);
|
|
|
|
// The test signature has an alias function, which expands subcommand "loop1" to "loop2", which
|
|
// itself is an alias that expands back to "loop1".
|
|
assert!(complete_at_end_of_line("test loop1 ", &ctx).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_command_completions_includes_abbreviations() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry)
|
|
.with_abbreviations(HashMap::from_iter([("gl".into(), "git log".into())]))
|
|
.with_top_level_commands(vec!["git", "gl"]);
|
|
|
|
let line = "g";
|
|
let suggestion = suggestions_for_test(
|
|
line,
|
|
line.len(),
|
|
CompleterOptions {
|
|
match_strategy: MatchStrategy::CaseInsensitive,
|
|
fallback_strategy: CompletionsFallbackStrategy::FilePaths,
|
|
suggest_file_path_completions_only: false,
|
|
parse_quotes_as_literals: false,
|
|
},
|
|
&ctx,
|
|
)
|
|
.into_iter()
|
|
.flat_map(|res| res.suggestions)
|
|
.find(|s| s.display() == "gl");
|
|
|
|
let suggestion = suggestion.unwrap();
|
|
// Assert that a suggestion was created for the abbreviation.
|
|
assert_eq!(suggestion.display(), "gl");
|
|
// The replacement span should be the expanded form of the abbreviation.
|
|
assert_eq!(suggestion.replacement(), "git log");
|
|
}
|
|
|
|
#[test]
|
|
fn test_replacement_span() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
assert_eq!(
|
|
complete_replacement_span("test two ", &ctx),
|
|
Some(Span::new(9, 9))
|
|
);
|
|
assert_eq!(
|
|
complete_replacement_span("test two two-one ", &ctx),
|
|
Some(Span::new(17, 17))
|
|
);
|
|
assert_eq!(
|
|
complete_replacement_span("test four ", &ctx),
|
|
Some(Span::new(10, 10))
|
|
);
|
|
assert_eq!(
|
|
complete_replacement_span("test four four-one ", &ctx),
|
|
Some(Span::new(19, 19))
|
|
);
|
|
assert_eq!(
|
|
complete_replacement_span("test four four-one four-", &ctx),
|
|
Some(Span::new(19, 24))
|
|
);
|
|
assert_eq!(
|
|
complete_replacement_span("test four four-one four-two ", &ctx),
|
|
Some(Span::new(29, 29))
|
|
);
|
|
assert_eq!(
|
|
complete_replacement_span("test four four-one four-two four-two four-", &ctx),
|
|
Some(Span::new(37, 42))
|
|
);
|
|
}
|
|
|
|
/// Verifies that we show completions for top level commands if we are completing on a command that
|
|
/// has an argument where `is_command` is true.
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
pub fn test_completion_results_shows_top_level_commands_for_is_command_argument() {
|
|
let registry = CommandRegistry::default();
|
|
let ctx =
|
|
FakeCompletionContext::new(registry).with_top_level_commands(["git", "sudo", "command"]);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("sudo ", &ctx),
|
|
vec!["command", "git", "sudo"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("ENV=FOO sudo ", &ctx),
|
|
vec!["command", "git", "sudo"]
|
|
);
|
|
|
|
// Multiple commands that have arguments that are top-level commands (in this case `sudo` and
|
|
// `command`) should continue to surface top level commands.
|
|
assert_eq!(
|
|
complete_at_end_of_line("sudo command ", &ctx),
|
|
vec!["command", "git", "sudo"]
|
|
);
|
|
}
|
|
|
|
/// Verifies that we only surface a commands's static list of arguments (instead of all top level commands)
|
|
/// if a command's argument is marked as `is_command: true` but also has a list of static `ArgumentTypes`.
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
pub fn test_completion_results_shows_arguments_for_is_command_argument() {
|
|
let registry = CommandRegistry::default();
|
|
registry.register_signature(test_signature());
|
|
registry.register_signature(git_signature());
|
|
let ctx =
|
|
FakeCompletionContext::new(registry).with_top_level_commands(["git", "sudo", "command"]);
|
|
|
|
// The `test nine` signature has a single argument that is marked as `is_command: true` but also has a single static
|
|
// argument of "git". In this case we should only show "git", instead of all possible top level commands.
|
|
assert_eq!(
|
|
complete_at_end_of_line("sudo test nine git", &ctx),
|
|
vec!["git"]
|
|
);
|
|
|
|
// We should still surface completions for git (because the `test nine` signature is marked with `is_command: true`)
|
|
assert_eq!(
|
|
complete_at_end_of_line("sudo test nine git ", &ctx),
|
|
vec!["add", "branch", "checkout", "clone"]
|
|
);
|
|
}
|
|
|
|
/// Verifies that we show subcommand / argument completions for an argument that is actually a top
|
|
/// level command.
|
|
/// e.g. `sudo git ` should surface completions for _git_.
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
pub fn test_completion_results_for_is_command_argument() {
|
|
// Create the normal command registry with our custom `git` signature.
|
|
let registry = CommandRegistry::default();
|
|
registry.register_signature(git_signature());
|
|
|
|
let ctx = FakeCompletionContext::new(registry).with_top_level_commands(["git", "sudo"]);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("sudo git ", &ctx),
|
|
vec!["add", "branch", "checkout", "clone"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("sudo git c", &ctx),
|
|
vec!["checkout", "clone"]
|
|
);
|
|
|
|
// Completions for subcommands of the new top-level command should also work.
|
|
assert_eq!(
|
|
complete_at_end_of_line("sudo git checkout ", &ctx),
|
|
vec!["漢字", "bob/卡b卡"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_variadic_args() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
assert_eq!(complete_at_end_of_line("test two ", &ctx), vec!["two-one"]);
|
|
|
|
// Even though the first positional is repeating, because there's another positional
|
|
// after this one, we complete on that.
|
|
assert_eq!(
|
|
complete_at_end_of_line("test two two-one ", &ctx),
|
|
vec!["two-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test four ", &ctx),
|
|
vec!["four-one"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test four four-one ", &ctx),
|
|
vec!["four-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test four four-one four-", &ctx),
|
|
vec!["four-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test four four-one four-two ", &ctx),
|
|
vec!["four-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test four four-one four-two four-two ", &ctx),
|
|
vec!["four-two"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_variadic_args_for_option() {
|
|
let registry = create_test_command_registry([git_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("git branch --delete ", &ctx),
|
|
vec!["branch-1", "second-branch"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("git branch --delete branch-1 ", &ctx),
|
|
vec!["branch-1", "second-branch"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("git branch --delete branch-1 s", &ctx),
|
|
vec!["second-branch"]
|
|
);
|
|
}
|
|
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
pub fn test_equal_sign_flag_does_not_bleed_variadic_suggestions() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
// `--long` has variadic args ["long-one", "long-two"]. With space-delimited form,
|
|
// additional values ARE expected after the first:
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --long long-one ", &ctx),
|
|
vec!["long-one", "long-two"]
|
|
);
|
|
|
|
// But with the '=' form, the flag is self-contained. The next token should get
|
|
// subcommand completions, not more --long values.
|
|
let mut eq_results = complete_at_end_of_line("test --long=long-one ", &ctx);
|
|
eq_results.sort();
|
|
assert_eq!(
|
|
eq_results,
|
|
vec!["eight", "five", "four", "nine", "one", "seven", "six", "three", "two"]
|
|
);
|
|
}
|
|
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
pub fn test_equal_sign_flag_completions() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --long=long-o", &ctx),
|
|
vec!["long-one"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --long=", &ctx),
|
|
vec!["long-one", "long-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test -r --long=", &ctx),
|
|
vec!["long-one", "long-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test -r -V --long=long-o", &ctx),
|
|
vec!["long-one"]
|
|
);
|
|
}
|
|
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
pub fn test_equal_sign_flag_mixed_with_other_styles() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --not-long=bar --long=", &ctx),
|
|
vec!["long-one", "long-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --not-long bar --long=", &ctx),
|
|
vec!["long-one", "long-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --long=foo --not-long ", &ctx),
|
|
vec!["not-long-one", "not-long-two"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test -r --not-long bar --long=long-o", &ctx),
|
|
vec!["long-one"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test -r --not-long=bar --long=", &ctx),
|
|
vec!["long-one", "long-two"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_multiple_required_args_for_option() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --required-args ", &ctx),
|
|
vec!["arg-1-1", "arg-1-2"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --required-args arg-1-1 ", &ctx),
|
|
vec!["arg-2-1", "arg-2-2"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --required-args arg-1-1 a", &ctx),
|
|
vec!["arg-2-1", "arg-2-2"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_required_arg_with_variadic_arg_for_option() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --required-args-with-var ", &ctx),
|
|
vec!["arg-1", "arg-2"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_template_args_for_arg_for_option() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_dir("foo"),
|
|
EngineDirEntry::test_file("bar"),
|
|
])
|
|
.with_entries(
|
|
pwd.join("foo/"),
|
|
[
|
|
EngineDirEntry::test_dir("src"),
|
|
EngineDirEntry::test_file("hidden"),
|
|
],
|
|
);
|
|
let ctx = FakeCompletionContext::new(registry).with_path_completion_context(path_ctx);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --template-args-for-opt ", &ctx),
|
|
vec!["bar", "foo/"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --template-args-for-opt foo/", &ctx),
|
|
vec!["hidden", "src/"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_complete_last_arg_after_non_variadic_option() {
|
|
let registry = create_test_command_registry([ls_signature()]);
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_dir("foo"),
|
|
EngineDirEntry::test_file("bar"),
|
|
])
|
|
.with_entries(
|
|
pwd.join("foo/"),
|
|
[
|
|
EngineDirEntry::test_dir("src"),
|
|
EngineDirEntry::test_file("hidden"),
|
|
],
|
|
);
|
|
let ctx = FakeCompletionContext::new(registry).with_path_completion_context(path_ctx);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("ls --color=auto ", &ctx),
|
|
vec!["bar", "foo/"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("ls --color=auto f", &ctx),
|
|
vec!["foo/"]
|
|
);
|
|
}
|
|
|
|
/// TODO(CORE-646): The following two tests are ignored because they are failing. They
|
|
/// test the scenario where an option argument is optional or variadic, and so
|
|
/// it is ambiguous whether the user is trying to complete the option argument
|
|
/// or the next command argument. In this scenario, we should show suggestions
|
|
/// for both argument types.
|
|
#[ignore]
|
|
#[test]
|
|
pub fn test_complete_last_arg_after_optional_non_variadic_option() {
|
|
let registry = create_test_command_registry([ls_signature()]);
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_dir("foo"),
|
|
EngineDirEntry::test_file("bar"),
|
|
])
|
|
.with_entries(
|
|
pwd.join("foo/"),
|
|
[
|
|
EngineDirEntry::test_dir("src"),
|
|
EngineDirEntry::test_file("hidden"),
|
|
],
|
|
);
|
|
let ctx = FakeCompletionContext::new(registry).with_path_completion_context(path_ctx);
|
|
|
|
// --color has an optional argument that must be supplied using `=`.
|
|
// Since no `=` was supplied, we should complete on the next command
|
|
// argument.
|
|
assert_eq!(
|
|
complete_at_end_of_line("ls --color ", &ctx),
|
|
vec!["bar", "foo/"]
|
|
);
|
|
|
|
assert_eq!(complete_at_end_of_line("ls --color f", &ctx), vec!["foo/"]);
|
|
}
|
|
|
|
/// See above test.
|
|
#[ignore]
|
|
#[test]
|
|
pub fn test_complete_last_arg_after_variadic_option() {
|
|
let registry = create_test_command_registry([ls_signature()]);
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_dir("foo"),
|
|
EngineDirEntry::test_file("bar"),
|
|
])
|
|
.with_entries(
|
|
pwd.join("foo/"),
|
|
[
|
|
EngineDirEntry::test_dir("src"),
|
|
EngineDirEntry::test_file("hidden"),
|
|
],
|
|
);
|
|
let ctx = FakeCompletionContext::new(registry).with_path_completion_context(path_ctx);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("ls --test auto f", &ctx),
|
|
vec!["force", "foo/"]
|
|
);
|
|
}
|
|
|
|
/// TODO: we should fix these failing tests. These tests are currently failing
|
|
/// because we don't have a way of computing the positional index correctly
|
|
/// when nesting arguments under options.
|
|
/// See more here: https://linear.app/warpdotdev/issue/WAR-3660/fix-completions-for-arguments-under-options
|
|
#[ignore]
|
|
#[test]
|
|
pub fn test_completions_after_arguments_under_option() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
// Required args only
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --required-args arg-1-1 arg-2-2 ", &ctx),
|
|
vec!["four", "one", "three", "two"]
|
|
);
|
|
|
|
// Required args followed by variadic arg
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --required-args-with-var arg-1 ", &ctx),
|
|
vec!["vararg-1", "vararg-2"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --required-args-with-var arg-1-1 vararg-1 ", &ctx),
|
|
vec!["vararg-1", "vararg-2"]
|
|
);
|
|
}
|
|
|
|
/// This suffers from the same problem above.
|
|
#[ignore]
|
|
#[test]
|
|
pub fn test_required_and_optional_args_for_option() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test --required-and-optional-args required-1 ", &ctx),
|
|
vec!["four", "one", "optional-1", "optional-2", "three", "two"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_ls() {
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_file("foo"),
|
|
EngineDirEntry::test_file("foobar"),
|
|
EngineDirEntry::test_dir("src"),
|
|
])
|
|
.with_entries(pwd.join("src/"), [EngineDirEntry::test_file("buzz")]);
|
|
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
|
.with_path_completion_context(path_ctx);
|
|
|
|
// "ls" with no flags should suggest files and folders
|
|
assert_eq!(
|
|
complete_at_end_of_line("ls ", &ctx),
|
|
vec!["foo", "foobar", "src/"]
|
|
);
|
|
|
|
// "ls -l " should still suggest filenames
|
|
assert_eq!(
|
|
complete_at_end_of_line("ls -l ", &ctx),
|
|
vec!["foo", "foobar", "src/"]
|
|
);
|
|
|
|
// Multiple flags should not affect completion results on args.
|
|
assert_eq!(
|
|
complete_at_end_of_line("ls -l -a ", &ctx),
|
|
vec!["foo", "foobar", "src/"]
|
|
);
|
|
|
|
// Since the positional arg in ls is repeating, we should keep on suggesting filepaths
|
|
// even if multiple args are already supplied.
|
|
assert_eq!(
|
|
complete_at_end_of_line("ls -l -a foo foo foo ", &ctx),
|
|
vec!["foo", "foobar", "src/"]
|
|
);
|
|
}
|
|
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
pub fn test_mv() {
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_file("foo"),
|
|
EngineDirEntry::test_file("foobar"),
|
|
EngineDirEntry::test_dir("src"),
|
|
])
|
|
.with_entries(pwd.join("src/"), [EngineDirEntry::test_file("buzz")]);
|
|
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
|
.with_path_completion_context(path_ctx);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("mv ", &ctx),
|
|
vec!["foo", "foobar", "src/"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("mv foo ", &ctx),
|
|
vec!["foo", "foobar", "src/"]
|
|
);
|
|
|
|
assert_eq!(complete_at_end_of_line("mv foo sr", &ctx), vec!["src/"]);
|
|
|
|
// Adding flags should still suggest filepaths correctly.
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options("mv -", MatchStrategy::CaseInsensitive, &ctx),
|
|
vec!["-f", "-i", "-n", "-v"]
|
|
);
|
|
assert_eq!(
|
|
complete_at_end_of_line("mv -R ", &ctx),
|
|
vec!["foo", "foobar", "src/"]
|
|
);
|
|
assert_eq!(
|
|
complete_at_end_of_line("mv -R f", &ctx),
|
|
vec!["foo", "foobar"]
|
|
);
|
|
assert_eq!(
|
|
complete_at_end_of_line("mv -R foo ", &ctx),
|
|
vec!["foo", "foobar", "src/"]
|
|
);
|
|
assert_eq!(
|
|
complete_at_end_of_line("mv -R foo src/", &ctx),
|
|
vec!["buzz"]
|
|
);
|
|
}
|
|
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
pub fn test_env_var_completion() {
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_home_directory(TEST_WORK_DIR.to_owned())
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_file("Cargo.toml"),
|
|
EngineDirEntry::test_dir("target"),
|
|
EngineDirEntry::test_dir("src"),
|
|
])
|
|
.with_entries(pwd.join("src/"), [EngineDirEntry::test_dir("app")])
|
|
.with_entries(pwd.join("src/app"), [EngineDirEntry::test_file("mod.rs")])
|
|
.with_entries(
|
|
pwd.join("target"),
|
|
[
|
|
EngineDirEntry::test_dir("debug"),
|
|
EngineDirEntry::test_dir("release"),
|
|
],
|
|
)
|
|
.with_entries(
|
|
pwd.join("target/debug"),
|
|
[EngineDirEntry::test_file("galaxyui")],
|
|
);
|
|
|
|
let env_vars = HashSet::from_iter([("HOME".into()), ("BAR".into()), ("BAZZ".into())]);
|
|
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
|
.with_path_completion_context(path_ctx)
|
|
.with_top_level_commands(["cargo", "cd", "git"])
|
|
.with_environment_variable_names(env_vars);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("cd $", &ctx),
|
|
vec!["$BAR", "$BAZZ", "$HOME"]
|
|
);
|
|
assert_eq!(
|
|
complete_at_end_of_line("cd $B", &ctx),
|
|
vec!["$BAR", "$BAZZ"]
|
|
);
|
|
|
|
assert_eq!(complete_at_end_of_line("cd $BAZ", &ctx), vec!["$BAZZ"]);
|
|
|
|
assert!(complete_at_end_of_line("cd $BUZZ", &ctx).is_empty());
|
|
|
|
// Completions for env vars should work for a command that is not an internal commamd.
|
|
assert_eq!(
|
|
complete_at_end_of_line("rustfmt $", &ctx),
|
|
vec!["$BAR", "$BAZZ", "$HOME"]
|
|
);
|
|
assert_eq!(
|
|
complete_at_end_of_line("rustfmt $B", &ctx),
|
|
vec!["$BAR", "$BAZZ"]
|
|
);
|
|
|
|
assert_eq!(complete_at_end_of_line("rustfmt $BAZ", &ctx), vec!["$BAZZ"]);
|
|
|
|
// Env vars should complete even if there is no command entered
|
|
assert_eq!(
|
|
complete_at_end_of_line("$", &ctx),
|
|
vec!["$BAR", "$BAZZ", "$HOME"]
|
|
);
|
|
assert_eq!(complete_at_end_of_line("$B", &ctx), vec!["$BAR", "$BAZZ"]);
|
|
|
|
assert_eq!(complete_at_end_of_line("$BAZ", &ctx), vec!["$BAZZ"]);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("cd $HOME/", &ctx),
|
|
vec!["src/", "target/"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("$HOME/", &ctx),
|
|
vec!["Cargo.toml", "src/", "target/"]
|
|
);
|
|
}
|
|
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
pub fn test_alias_completion() {
|
|
let registry = CommandRegistry::default();
|
|
registry.register_signature(test_signature());
|
|
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_home_directory(TEST_WORK_DIR.to_owned())
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_file("Cargo.toml"),
|
|
EngineDirEntry::test_dir("target"),
|
|
EngineDirEntry::test_dir("src"),
|
|
])
|
|
.with_entries(pwd.join("src/"), [EngineDirEntry::test_dir("app")])
|
|
.with_entries(pwd.join("src/app"), [EngineDirEntry::test_file("mod.rs")])
|
|
.with_entries(
|
|
pwd.join("target"),
|
|
[
|
|
EngineDirEntry::test_dir("debug"),
|
|
EngineDirEntry::test_dir("release"),
|
|
],
|
|
)
|
|
.with_entries(
|
|
pwd.join("target/debug"),
|
|
[EngineDirEntry::test_file("galaxyui")],
|
|
);
|
|
|
|
let aliases = HashMap::from_iter([
|
|
("first".into(), "test one".to_string()),
|
|
("second".into(), "first".to_string()),
|
|
("third".into(), "cd".to_string()),
|
|
("ls".into(), "ls -l".to_string()),
|
|
]);
|
|
let ctx = FakeCompletionContext::new(registry)
|
|
.with_path_completion_context(path_ctx)
|
|
.with_aliases(aliases)
|
|
.with_top_level_commands(["cargo", "cd", "git", "ls", "first", "second", "third"]);
|
|
|
|
// An alias that expands to itself should not terminate and only expand once.
|
|
assert_eq!(
|
|
complete_at_end_of_line("ls ", &ctx),
|
|
vec!["Cargo.toml", "src/", "target/"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_environment_variable_assignment() {
|
|
let registry = create_test_command_registry([git_signature()]);
|
|
let path_ctx = MockPathCompletionContext::new(TypedPathBuf::from(TEST_WORK_DIR))
|
|
.with_home_directory(TEST_WORK_DIR.to_owned())
|
|
.with_entries_in_pwd([EngineDirEntry::test_file("Cargo.toml")]);
|
|
let aliases = HashMap::from_iter([("git".into(), "foo=Foo git".into())]);
|
|
let ctx = FakeCompletionContext::new(registry)
|
|
.with_path_completion_context(path_ctx)
|
|
.with_aliases(aliases)
|
|
.with_top_level_commands(["git"]);
|
|
|
|
// Completion with environment variable assignment should work.
|
|
assert_eq!(
|
|
complete_at_end_of_line("foo=Foo git ", &ctx),
|
|
vec!["add", "branch", "checkout", "clone"]
|
|
);
|
|
|
|
// Completion with multiple environment variable assignments should work.
|
|
assert_eq!(
|
|
complete_at_end_of_line("foo=Foo bar=Bar git ", &ctx),
|
|
vec!["add", "branch", "checkout", "clone"]
|
|
);
|
|
|
|
// Completion with alias that expands to commands with environment variable assignment should work.
|
|
assert_eq!(
|
|
complete_at_end_of_line("git ", &ctx),
|
|
vec!["add", "branch", "checkout", "clone"]
|
|
);
|
|
|
|
// Completion that includes environment variable assignment in the wrong position should not work.
|
|
assert_eq!(
|
|
complete_at_end_of_line("git foo=Foo ", &ctx),
|
|
vec!["Cargo.toml"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_environment_variable_assignment_no_command() {
|
|
let path_ctx = MockPathCompletionContext::new(TypedPathBuf::from(TEST_WORK_DIR))
|
|
.with_home_directory(TEST_WORK_DIR.to_owned())
|
|
.with_entries_in_pwd([EngineDirEntry::test_file("Cargo.toml")]);
|
|
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
|
.with_path_completion_context(path_ctx);
|
|
|
|
// Completion with multiple environment variable assignments and no command
|
|
// shouldn't result in an error.
|
|
assert!(complete_at_end_of_line("foo=Foo bar=Bar", &ctx).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_path_fallback() {
|
|
let registry = create_test_command_registry([signature_with_empty_positional()]);
|
|
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_home_directory(TEST_WORK_DIR.to_owned())
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_file("Cargo.toml"),
|
|
EngineDirEntry::test_dir("target"),
|
|
EngineDirEntry::test_dir("src"),
|
|
])
|
|
.with_entries(pwd.join("src/"), [EngineDirEntry::test_dir("app")])
|
|
.with_entries(pwd.join("src/app"), [EngineDirEntry::test_file("mod.rs")])
|
|
.with_entries(
|
|
pwd.join("target"),
|
|
[
|
|
EngineDirEntry::test_dir("debug"),
|
|
EngineDirEntry::test_dir("release"),
|
|
],
|
|
)
|
|
.with_entries(
|
|
pwd.join("target/debug"),
|
|
[EngineDirEntry::test_file("galaxyui")],
|
|
);
|
|
|
|
let ctx = FakeCompletionContext::new(registry).with_path_completion_context(path_ctx);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("rustfmt ", &ctx),
|
|
vec!["Cargo.toml", "src/", "target/"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("rustfmt --check ", &ctx),
|
|
vec!["Cargo.toml", "src/", "target/"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("rustfmt --check C", &ctx),
|
|
vec!["Cargo.toml"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("rustfmt --check C", &ctx),
|
|
vec!["Cargo.toml"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test-empty ", &ctx),
|
|
vec!["Cargo.toml", "src/", "target/"]
|
|
);
|
|
|
|
// `test-empty` has a single positional but no completers so ensure we just fallback to paths.
|
|
assert_eq!(complete_at_end_of_line("test-empty s", &ctx), vec!["src/"]);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test-empty -m ", &ctx),
|
|
vec!["Cargo.toml", "src/", "target/"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test-empty -m s", &ctx),
|
|
vec!["src/"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_autocd() {
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_home_directory(TEST_WORK_DIR.to_owned())
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_file("Cargo.toml"),
|
|
EngineDirEntry::test_dir("certs"),
|
|
EngineDirEntry::test_dir("src"),
|
|
])
|
|
.with_entries(pwd.join("src/"), [EngineDirEntry::test_dir("app")]);
|
|
|
|
let registry = create_test_command_registry([]);
|
|
let mut ctx = FakeCompletionContext::new(registry)
|
|
.with_path_completion_context(path_ctx)
|
|
.with_top_level_commands(["cargo", "cd", "git"])
|
|
.with_supports_autocd(true);
|
|
|
|
// Ensure that `certs` (a directory) is suggested because `autocd` is enabled, but
|
|
// `Cargo.toml` is not because it is a file.
|
|
assert_eq!(
|
|
complete_at_end_of_line("c", &ctx),
|
|
vec!["cargo", "cd", "certs/",]
|
|
);
|
|
|
|
assert_eq!(complete_at_end_of_line("ce", &ctx), vec!["certs/",]);
|
|
|
|
ctx = ctx.with_supports_autocd(false);
|
|
|
|
// `certs` should no longer be suggested because `auto_cd` is turned off.
|
|
assert_eq!(complete_at_end_of_line("c", &ctx), vec!["cargo", "cd",]);
|
|
assert!(complete_at_end_of_line("ce", &ctx).is_empty());
|
|
}
|
|
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
pub fn test_completions() {
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_home_directory(TEST_WORK_DIR.to_owned())
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_file("Cargo.toml"),
|
|
EngineDirEntry::test_dir("target"),
|
|
EngineDirEntry::test_dir("src"),
|
|
])
|
|
.with_entries(pwd.join("src/"), [EngineDirEntry::test_dir("app")])
|
|
.with_entries(pwd.join("src/app"), [EngineDirEntry::test_file("mod.rs")])
|
|
.with_entries(
|
|
pwd.join("target"),
|
|
[
|
|
EngineDirEntry::test_dir("debug"),
|
|
EngineDirEntry::test_dir("release"),
|
|
],
|
|
)
|
|
.with_entries(
|
|
pwd.join("target/debug"),
|
|
[EngineDirEntry::test_file("galaxyui")],
|
|
);
|
|
|
|
let registry = create_test_command_registry([git_signature(), cd_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry)
|
|
.with_path_completion_context(path_ctx)
|
|
.with_top_level_commands(["cargo", "cd", "git"]);
|
|
|
|
assert!(complete_at_end_of_line("cde", &ctx).is_empty());
|
|
|
|
assert_eq!(complete_at_end_of_line("c", &ctx), vec!["cargo", "cd"]);
|
|
|
|
assert_eq!(complete_at_end_of_line("git", &ctx), vec!["git"]);
|
|
|
|
// The "-" here is included, even though it is hidden. It gets removed by a subsequent
|
|
// `filter_by_query` call, which isn't tested here.
|
|
assert_eq!(
|
|
complete_at_end_of_line("cd ", &ctx),
|
|
vec!["-", "src/", "target/"]
|
|
);
|
|
|
|
assert_eq!(complete_at_end_of_line("cd sr", &ctx), vec!["src/"]);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("cd target/de", &ctx),
|
|
vec!["debug/"]
|
|
);
|
|
|
|
#[cfg(unix)]
|
|
assert!(complete_at_end_of_line("cd /", &ctx).contains(&TEST_ROOT_DIR.to_owned()));
|
|
|
|
// TODO(CORE-3696): test Windows root directory separately
|
|
// #[cfg(windows)]
|
|
// assert!(complete_at_end_of_line("cd C:", &ctx).contains(&TEST_ROOT_DIR.to_owned()));
|
|
|
|
let git_subcommands = vec!["add", "branch", "checkout", "clone"];
|
|
assert_eq!(complete_at_end_of_line("git ", &ctx), git_subcommands);
|
|
|
|
assert_eq!(complete_at_end_of_line("git ad", &ctx), vec!["add"]);
|
|
assert_eq!(
|
|
complete_at_end_of_line("git c", &ctx),
|
|
vec!["checkout", "clone"]
|
|
);
|
|
assert!(complete_at_end_of_line("git missing", &ctx).is_empty());
|
|
|
|
assert_eq!(complete_at_end_of_line("cd ; gi", &ctx), vec!["git"]);
|
|
|
|
assert_eq!(complete_at_end_of_line("cd ; git ", &ctx), git_subcommands);
|
|
assert_eq!(
|
|
complete_at_end_of_line("cd foo && git ", &ctx),
|
|
git_subcommands
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("cd foo ; git check", &ctx),
|
|
vec!["checkout"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line(
|
|
r#"ls -la && cat hello.txt
|
|
echo $(ls -la); cat password.secret | pbcopy
|
|
echo $(git check"#,
|
|
&ctx
|
|
),
|
|
vec!["checkout"]
|
|
);
|
|
/* end argument completer calls */
|
|
}
|
|
|
|
#[test]
|
|
fn completes_arguments() {
|
|
let registry = create_test_command_registry([git_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("ls -la && git c", &ctx),
|
|
vec!["checkout", "clone"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("ls -la || echo $(git c", &ctx),
|
|
vec!["checkout", "clone"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_completes_complicated_commands() {
|
|
let registry = create_test_command_registry([]);
|
|
let ctx = FakeCompletionContext::new(registry).with_top_level_commands(["cargo", "cd", "git"]);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("ls -la | c", &ctx),
|
|
vec!["cargo", "cd"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("ls -la\necho $(c", &ctx),
|
|
vec!["cargo", "cd"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn completes_many_args_under_option() {
|
|
let registry = create_test_command_registry([git_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("git branch -m ", &ctx),
|
|
vec!["branch-1", "second-branch"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("git branch -m b", &ctx),
|
|
vec!["branch-1"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("git branch -m branch-1 ", &ctx),
|
|
vec!["branch-1", "second-branch"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("git branch -m branch-1 s", &ctx),
|
|
vec!["second-branch"]
|
|
);
|
|
}
|
|
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
fn test_hidden_suggestion_only_appears_on_exact_match() {
|
|
let registry = create_test_command_registry([cd_signature()]);
|
|
let path_ctx = MockPathCompletionContext::new(TypedPathBuf::from(TEST_WORK_DIR))
|
|
.with_entries_in_pwd([EngineDirEntry::test_dir("app")]);
|
|
let ctx = FakeCompletionContext::new(registry).with_path_completion_context(path_ctx);
|
|
|
|
// Need to use filter_by_query here to incorporate the the exact match logic.
|
|
let suggestion_results =
|
|
complete_at_end_of_line_with_query("cd ", "", MatchStrategy::CaseInsensitive, &ctx);
|
|
assert_eq!(suggestion_results, vec!["app/"]);
|
|
|
|
let suggestion_results =
|
|
complete_at_end_of_line_with_query("cd ", "-", MatchStrategy::CaseInsensitive, &ctx);
|
|
assert_eq!(suggestion_results, vec!["-"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_subcommands_with_prefixes_of_one_another_are_completed_correctly() {
|
|
let registry = create_test_command_registry([npm_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
assert_eq!(complete_at_end_of_line("npm r", &ctx), vec!["r", "run"]);
|
|
|
|
assert_eq!(complete_at_end_of_line("npm r ", &ctx), vec!["r-arg"]);
|
|
|
|
assert_eq!(complete_at_end_of_line("npm ru", &ctx), vec!["run"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_completes_with_correct_ordering() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
// Make sure the ordering is working as expected
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options("test ", MatchStrategy::CaseInsensitive, &ctx),
|
|
vec![
|
|
// High importance suggestions appear before default suggestions. Since they are tied in
|
|
// priority, we sort them lexicographically.
|
|
"--not-long", // Priority 100, option
|
|
"--required-args", // Priority 100, option
|
|
"one", // Priority 100, subcommand
|
|
// Default priority suggestions retain the order produced by the completion engine,
|
|
// sorted by `SuggestionType`.
|
|
"eight", // Priority default, subcommand
|
|
"five", // Priority default, subcommand
|
|
"four", // Priority default, subcommand
|
|
"nine", // Priority default, subcommand
|
|
"seven", // Priority default, subcommand
|
|
"six", // Priority default, subcommand
|
|
"--required-and-optional-args", // Priority default, option
|
|
"--template-args-for-opt", // Priority default, option
|
|
"-r", // Priority default, option
|
|
"-V", // Priority default, option
|
|
// Low importance suggestions appear after default suggestions.
|
|
"two", // Priority 50, subcommand
|
|
// Tied in order so we sort lexicographically.
|
|
"--long", // Priority 1, option
|
|
"--required-args-with-var", // Priority 1, option
|
|
"three", // Priority 1, subcommand
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_completes_with_multiple_generators() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let generator_ctx = MockGeneratorContext::for_test_signature();
|
|
let ctx = FakeCompletionContext::new(registry).with_generator_context(generator_ctx);
|
|
|
|
// Make sure the new ordering is working as expected
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options("test five ", MatchStrategy::CaseInsensitive, &ctx),
|
|
vec![
|
|
// There are argument suggestions. They come from two generators,
|
|
// so we should respect the order in which the generators were specified.
|
|
"bar", "foo", // The first generator is not ordered, so we sort it.
|
|
"def", "abc", // The second generator is already ordered.
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_completes_flags() {
|
|
let registry = create_test_command_registry([git_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
// Should complete all subcommands followed by all long hand flags followed by all short hand flags
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options("git ", MatchStrategy::CaseInsensitive, &ctx),
|
|
vec![
|
|
"add",
|
|
"branch",
|
|
"checkout",
|
|
"clone",
|
|
"--bare",
|
|
"--help",
|
|
"--version",
|
|
"-c",
|
|
"-p",
|
|
]
|
|
);
|
|
|
|
// But any non-dash prefix should prevent showing flags by default
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options("git c", MatchStrategy::CaseInsensitive, &ctx),
|
|
vec!["checkout", "clone",]
|
|
);
|
|
|
|
// Should complete all short hand flags followed by all long hand flags
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options("ls -la; git -", MatchStrategy::CaseInsensitive, &ctx),
|
|
vec!["-c", "-p", "--bare", "--help", "--version",]
|
|
);
|
|
|
|
// Should complete all not included short hand flags and the last flag.
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options(
|
|
"ls -la; git -p",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
vec!["-c", "-p"]
|
|
);
|
|
|
|
// Should not complete short hand flags that are already included except for the
|
|
// last flag.
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options(
|
|
"ls -la; git -pc",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
vec!["-c"]
|
|
);
|
|
|
|
// Should complete all long hand flags only
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options(
|
|
"ls -la; git --",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
vec!["--bare", "--help", "--version",]
|
|
);
|
|
|
|
// Should complete long hand flags only (that begin with "v")
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options(
|
|
"cat hello.txt | git --v",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
vec!["--version"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_complete_long_hand_flags_single_dash() {
|
|
let registry = create_test_command_registry([java_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
// Should complete long hand flags that use a single dash (and not add
|
|
// in the double-dashed versions).
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options("java -ve", MatchStrategy::CaseInsensitive, &ctx),
|
|
vec!["-version"]
|
|
);
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options("java -c", MatchStrategy::CaseInsensitive, &ctx),
|
|
vec!["-classpath", "-cp"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_matching_indices() {
|
|
let registry = create_test_command_registry([]);
|
|
let path_ctx = MockPathCompletionContext::new(TypedPathBuf::from(TEST_WORK_DIR))
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_dir("a卡b"),
|
|
EngineDirEntry::test_dir("卡b奇诺"),
|
|
EngineDirEntry::test_dir("诺卡b奇"),
|
|
EngineDirEntry::test_dir("奇诺卡b"),
|
|
]);
|
|
let ctx = FakeCompletionContext::new(registry).with_path_completion_context(path_ctx);
|
|
|
|
// We want to make sure that we are matching char indices
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query("cd 卡b", "卡b", MatchStrategy::CaseInsensitive, &ctx),
|
|
vec![(vec![0, 1], String::from("卡b奇诺/"))]
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query("cd 卡b", "卡b", MatchStrategy::CaseSensitive, &ctx),
|
|
vec![(vec![0, 1], String::from("卡b奇诺/"))]
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query("cd 卡b", "卡b", MatchStrategy::Fuzzy, &ctx),
|
|
vec![
|
|
(vec![0, 1], String::from("卡b奇诺/")),
|
|
(vec![1, 2], String::from("诺卡b奇/")),
|
|
(vec![2, 3], String::from("奇诺卡b/")),
|
|
(vec![1, 2], String::from("a卡b/")),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_matching_indices_nested_file_paths() {
|
|
let registry = create_test_command_registry([]);
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_dir("a卡b"),
|
|
EngineDirEntry::test_dir("dir"),
|
|
])
|
|
.with_entries(pwd.join("a卡b"), [EngineDirEntry::test_dir("c布d")])
|
|
.with_entries(
|
|
pwd.join("dir"),
|
|
[
|
|
EngineDirEntry::test_file("卡b奇诺"),
|
|
EngineDirEntry::test_file("诺卡b奇"),
|
|
EngineDirEntry::test_file("奇诺卡b"),
|
|
],
|
|
)
|
|
.with_entries(
|
|
pwd.join("a卡b/c布d"),
|
|
[
|
|
EngineDirEntry::test_file("f咖啡gm牛nhdj奶"),
|
|
EngineDirEntry::test_file("咖啡牛奶"),
|
|
],
|
|
);
|
|
let ctx = FakeCompletionContext::new(registry).with_path_completion_context(path_ctx);
|
|
|
|
// We want to make sure that we are matching char indices
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query(
|
|
"cd dir/",
|
|
"dir/卡b",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
vec![(vec![0, 1], String::from("卡b奇诺"))]
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query("cd dir/", "dir/卡b", MatchStrategy::Fuzzy, &ctx),
|
|
vec![
|
|
(vec![0, 1], String::from("卡b奇诺")),
|
|
(vec![1, 2], String::from("诺卡b奇")),
|
|
(vec![2, 3], String::from("奇诺卡b")),
|
|
]
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query("cd a卡b/", "a卡b/布", MatchStrategy::Fuzzy, &ctx),
|
|
vec![(vec![1], String::from("c布d/"))]
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query("cd a卡b/", "a卡b/cd", MatchStrategy::Fuzzy, &ctx),
|
|
vec![(vec![0, 2], String::from("c布d/"))]
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query(
|
|
"cd a卡b/",
|
|
"a卡b/cd",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
Vec::<(Vec<usize>, String)>::new()
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query(
|
|
"cd a卡b/c布d",
|
|
"a卡b/cd",
|
|
MatchStrategy::CaseSensitive,
|
|
&ctx
|
|
),
|
|
Vec::<(Vec<usize>, String)>::new()
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query(
|
|
"cd a卡b/c布d/",
|
|
"a卡b/c布d/咖啡牛奶",
|
|
MatchStrategy::Fuzzy,
|
|
&ctx
|
|
),
|
|
vec![
|
|
(vec![0, 1, 2, 3], String::from("咖啡牛奶")),
|
|
(vec![1, 2, 5, 10], String::from("f咖啡gm牛nhdj奶")),
|
|
]
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query(
|
|
"cd a卡b/c布d/",
|
|
"a卡b/c布d/咖啡牛奶",
|
|
MatchStrategy::CaseSensitive,
|
|
&ctx
|
|
),
|
|
vec![(vec![0, 1, 2, 3], String::from("咖啡牛奶"))]
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query(
|
|
"cd a卡b/c布d/",
|
|
"a卡b/c布d/咖啡牛奶",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
vec![(vec![0, 1, 2, 3], String::from("咖啡牛奶"))]
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query(
|
|
"cd a卡b/c布d/",
|
|
"a卡b/c布d/g牛h奶",
|
|
MatchStrategy::Fuzzy,
|
|
&ctx
|
|
),
|
|
vec![(vec![3, 5, 7, 10], String::from("f咖啡gm牛nhdj奶"))]
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query("cd a卡b/c布d/", "牛奶", MatchStrategy::Fuzzy, &ctx),
|
|
vec![
|
|
(vec![2, 3], String::from("咖啡牛奶")),
|
|
(vec![5, 10], String::from("f咖啡gm牛nhdj奶")),
|
|
]
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query(
|
|
"cd a卡b/c布d/",
|
|
"牛奶",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
Vec::<(Vec<usize>, String)>::new()
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query(
|
|
"cd a卡b/c布d/",
|
|
"a卡b/c布d/gdj",
|
|
MatchStrategy::Fuzzy,
|
|
&ctx
|
|
),
|
|
vec![(vec![3, 8, 9], String::from("f咖啡gm牛nhdj奶"))]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_matching_indices_for_git_branches() {
|
|
let registry = create_test_command_registry([git_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
// We want to make sure that we are matching char indices
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query(
|
|
"git checkout bob/卡",
|
|
"bob/卡",
|
|
MatchStrategy::Fuzzy,
|
|
&ctx
|
|
),
|
|
vec![(vec![0, 1, 2, 3, 4], String::from("bob/卡b卡"))]
|
|
);
|
|
|
|
assert_eq!(
|
|
get_filtered_suggestions_with_query(
|
|
"git checkout bob/b",
|
|
"bob/b",
|
|
MatchStrategy::Fuzzy,
|
|
&ctx
|
|
),
|
|
vec![(vec![0, 1, 2, 3, 5], String::from("bob/卡b卡"))]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fuzzy_completions() {
|
|
let registry = create_test_command_registry([fuzzy_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
// Prefix matches should still respect ordering logic
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options("fuzzy prefix", MatchStrategy::Fuzzy, &ctx),
|
|
vec!["prefix2", "prefix1", "suffix-pre-fix"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options("fuzzy prefix", MatchStrategy::CaseInsensitive, &ctx),
|
|
vec!["prefix2", "prefix1"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_exact_match_completions() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
// Neither suggestion is an exact match so just use regular prefix ordering
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options("test six si", MatchStrategy::CaseInsensitive, &ctx),
|
|
vec!["six-arg-2", "six-arg"]
|
|
);
|
|
|
|
// Exact match suggestion should be the first suggestion even if others have more priority.
|
|
// Need to use filter_by_query here to incorporate the the exact match logic.
|
|
let suggestions = complete_at_end_of_line_with_query(
|
|
"test six six-arg",
|
|
"six-arg",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx,
|
|
);
|
|
assert_eq!(suggestions, vec!["six-arg", "six-arg-2"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deduplication() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let generator_ctx = MockGeneratorContext::for_test_signature();
|
|
let ctx = FakeCompletionContext::new(registry).with_generator_context(generator_ctx);
|
|
|
|
// The arg for this subcommand has the same, ordered generator repeated twice.
|
|
// So if we weren't dedup'ing properly, then we would see ["def", "abc", "def", "abc"].
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options("test seven ", MatchStrategy::CaseInsensitive, &ctx),
|
|
vec!["def", "abc",]
|
|
);
|
|
}
|
|
|
|
/// Test to ensure that we also suggest subcommands if the user is in a positional where there
|
|
/// is an optional, variadic, argument.
|
|
#[test]
|
|
fn test_variadic_optional_arguments_also_completes_subcommands() {
|
|
let registry = create_test_command_registry([test_signature()]);
|
|
let ctx = FakeCompletionContext::new(registry);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test eight ", &ctx),
|
|
vec!["eight-arg"]
|
|
);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("test eight eight-arg ", &ctx),
|
|
vec!["eight-arg-2", "eight-subcommand"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_file_paths_with_separator_in_middle() {
|
|
let registry = create_test_command_registry([]);
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_entries_in_pwd([EngineDirEntry::test_dir("foo")])
|
|
.with_entries(
|
|
pwd.join("foo/"),
|
|
[
|
|
EngineDirEntry::test_file("script"),
|
|
EngineDirEntry::test_file("script1"),
|
|
],
|
|
);
|
|
let ctx = FakeCompletionContext::new(registry).with_path_completion_context(path_ctx.clone());
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line("foo/scri", &ctx),
|
|
vec!["script", "script1"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_case_sensitivity_ordering() {
|
|
let registry = create_test_command_registry([]);
|
|
let ctx = FakeCompletionContext::new(registry)
|
|
.with_top_level_commands(["git", "GIT", "gitter", "GITTER"]);
|
|
|
|
// The exact match appears first, followed by the case-insensitive exact match,
|
|
// followed by any prefix matches.
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_query("git", "git", MatchStrategy::Fuzzy, &ctx),
|
|
vec!["git", "GIT", "GITTER", "gitter"]
|
|
);
|
|
}
|
|
|
|
/// Regression test for Linear issue CORE-1885.
|
|
#[test]
|
|
pub fn test_autocd_completions_with_tilde() {
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
|
.with_home_directory(TEST_WORK_DIR.to_owned())
|
|
.with_entries_in_pwd([
|
|
EngineDirEntry::test_file("Cargo.toml"),
|
|
EngineDirEntry::test_dir("certs"),
|
|
EngineDirEntry::test_dir("src"),
|
|
])
|
|
.with_entries(pwd.join("src/"), [EngineDirEntry::test_dir("app")]);
|
|
|
|
let registry = create_test_command_registry([]);
|
|
let ctx = FakeCompletionContext::new(registry)
|
|
.with_path_completion_context(path_ctx)
|
|
.with_top_level_commands(["cargo", "cd", "git"])
|
|
.with_supports_autocd(true);
|
|
|
|
assert_eq!(complete_at_end_of_line("~/src/", &ctx), vec!["app/"]);
|
|
}
|
|
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
fn test_option_name_with_missing_required_value() {
|
|
let registry = create_test_command_registry([git_signature()]);
|
|
|
|
let path_ctx = MockPathCompletionContext::new(TypedPathBuf::from(TEST_WORK_DIR));
|
|
let ctx = FakeCompletionContext::new(registry).with_path_completion_context(path_ctx);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options(
|
|
"git branch --delete",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
vec!["--delete"]
|
|
);
|
|
}
|
|
|
|
/// TODO(CORE-2795)
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
fn test_powershell_parser_directives_for_flags() {
|
|
let registry = create_test_command_registry([add_content_signature()]);
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd).with_entries_in_pwd([
|
|
EngineDirEntry::test_dir("foo"),
|
|
EngineDirEntry::test_file("bar"),
|
|
]);
|
|
let ctx = FakeCompletionContext::new(registry).with_path_completion_context(path_ctx);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options(
|
|
"Add-Content -F",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
vec!["-Force"]
|
|
);
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options(
|
|
"Add-Content -Force -E",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
vec!["-Encoding", "-Exclude"]
|
|
);
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options(
|
|
"Add-Content -Force -En",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
vec!["-Encoding"]
|
|
);
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options(
|
|
"Add-Content -Force -Encoding",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
vec!["-Encoding"]
|
|
);
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options(
|
|
"Add-Content -Force -enc",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
vec!["-Encoding"]
|
|
);
|
|
assert_eq!(
|
|
complete_at_end_of_line("Add-Content -Force -Encoding ", &ctx),
|
|
vec!["ASCII", "UTF8"]
|
|
);
|
|
assert_eq!(
|
|
complete_at_end_of_line("Add-Content -Force -Enc ", &ctx),
|
|
vec!["ASCII", "UTF8"]
|
|
);
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options(
|
|
"Add-Content -F -Enc ASCII b",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
vec!["bar"]
|
|
);
|
|
}
|
|
|
|
/// TODO(CORE-2795)
|
|
#[cfg(not(feature = "v2"))]
|
|
#[test]
|
|
fn test_powershell_parser_directives_for_case_insensitivity() {
|
|
let registry = create_test_command_registry([add_content_signature()]);
|
|
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
|
let path_ctx = MockPathCompletionContext::new(pwd).with_entries_in_pwd([
|
|
EngineDirEntry::test_dir("foo"),
|
|
EngineDirEntry::test_file("bar"),
|
|
]);
|
|
let ctx = FakeCompletionContext::new(registry).with_path_completion_context(path_ctx);
|
|
|
|
assert_eq!(
|
|
complete_at_end_of_line_with_options(
|
|
"add-content -F",
|
|
MatchStrategy::CaseInsensitive,
|
|
&ctx
|
|
),
|
|
vec!["-Force"]
|
|
);
|
|
}
|