mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-06-19 04:41:15 -03:00
When trying to complete a command starting with `-`, and more
specifically when trying to get the description of possible commands,
the dash was interpreted as an option for `__fish_describe_command`,
resulting in an "unknown option" most of the time.
This is a regression introduced when adding option parsing to
`__fish_describe_command`
Fixes 7fc27e9e5 (cygwin: improve handling of `.exe` file extension, 2025-11-22)
Fixes #12510
Closes #12522
3380 lines
128 KiB
Rust
3380 lines
128 KiB
Rust
use std::{
|
|
cmp::Ordering,
|
|
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
|
mem,
|
|
ops::{Deref, DerefMut},
|
|
sync::{
|
|
LazyLock, Mutex, MutexGuard,
|
|
atomic::{self, AtomicUsize},
|
|
},
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use crate::{
|
|
abbrs::with_abbrs,
|
|
autoload::Autoload,
|
|
builtins::shared::{builtin_exists, builtin_get_desc, builtin_get_names},
|
|
common::{
|
|
ScopeGuard, UnescapeFlags, UnescapeStringStyle, escape, unescape_string,
|
|
valid_var_name_char,
|
|
},
|
|
env::{EnvMode, EnvStack, Environment},
|
|
exec::exec_subshell,
|
|
expand::{
|
|
ExpandFlags, ExpandResultCode, expand_escape_string, expand_escape_variable, expand_one,
|
|
expand_string, expand_to_receiver,
|
|
},
|
|
flog::{flog, flogf},
|
|
function,
|
|
history::{History, history_session_id},
|
|
operation_context::OperationContext,
|
|
parse_constants::SourceRange,
|
|
parse_util::{get_cmdsubst_extent, get_process_extent, unescape_wildcards},
|
|
parser::{Block, Parser, ParserEnvSetMode},
|
|
parser_keywords::parser_keywords_is_subcommand,
|
|
path::{path_get_path, path_try_get_path},
|
|
prelude::*,
|
|
tokenizer::{Tok, TokFlags, TokenType, Tokenizer, variable_assignment_equals_pos},
|
|
wildcard::{wildcard_complete, wildcard_has, wildcard_match},
|
|
wutil::wrealpath,
|
|
};
|
|
use crate::{
|
|
ast::unescape_keyword,
|
|
autoload::AutoloadResult,
|
|
common::charptr2wcstring,
|
|
localization::{LocalizableString, localizable_string},
|
|
reader::{get_quote, is_backslashed},
|
|
};
|
|
use assert_matches::assert_matches;
|
|
use bitflags::bitflags;
|
|
use fish_util::wcsfilecmp;
|
|
use fish_wcstringutil::{
|
|
StringFuzzyMatch, string_fuzzy_match_string, string_prefixes_string,
|
|
string_prefixes_string_case_insensitive, string_suffixes_string_case_insensitive,
|
|
strip_executable_suffix,
|
|
};
|
|
use fish_widestring::WExt as _;
|
|
|
|
// Completion description strings, mostly for different types of files, such as sockets, block
|
|
// devices, etc.
|
|
//
|
|
// There are a few more completion description strings defined in expand.rs. Maybe all completion
|
|
// description strings should be defined in the same file?
|
|
|
|
localizable_consts!(
|
|
/// Description for ~USER completion.
|
|
COMPLETE_USER_DESC "Home for %s"
|
|
|
|
/// Description for short variables. The value is concatenated to this description.
|
|
COMPLETE_VAR_DESC_VAL "Variable: %s"
|
|
|
|
/// Description for abbreviations.
|
|
ABBR_DESC "Abbreviation: %s"
|
|
);
|
|
|
|
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
|
|
pub struct CompletionMode {
|
|
/// If set, skip file completions.
|
|
pub no_files: bool,
|
|
pub force_files: bool,
|
|
|
|
/// If set, require a parameter after completion.
|
|
pub requires_param: bool,
|
|
}
|
|
|
|
/// Character that separates the completion and description on programmable completions.
|
|
pub const PROG_COMPLETE_SEP: char = '\t';
|
|
|
|
bitflags! {
|
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct CompleteFlags: u16 {
|
|
/// Do not insert space afterwards if this is the only completion. (The default is to try insert
|
|
/// a space).
|
|
const NO_SPACE = 1 << 0;
|
|
/// This is not the suffix of a token, but replaces it entirely.
|
|
const REPLACES_TOKEN = 1 << 1;
|
|
/// This completion may or may not want a space at the end - guess by checking the last
|
|
/// character of the completion.
|
|
const AUTO_SPACE = 1 << 2;
|
|
/// This completion should be inserted as-is, without escaping.
|
|
const DONT_ESCAPE = 1 << 3;
|
|
/// If you do escape, don't escape tildes.
|
|
const DONT_ESCAPE_TILDES = 1 << 4;
|
|
/// Do not sort supplied completions
|
|
const DONT_SORT = 1 << 5;
|
|
/// This completion looks to have the same string as an existing argument.
|
|
const DUPLICATES_ARGUMENT = 1 << 6;
|
|
/// This completes not just a token but replaces an entire line.
|
|
const REPLACES_LINE = 1 << 7;
|
|
/// If replacing the entire token, keep the "foo=" prefix.
|
|
const KEEP_VARIABLE_OVERRIDE_PREFIX = 1 << 8;
|
|
/// This is a variable name.
|
|
const VARIABLE_NAME = 1 << 9;
|
|
/// Suppress showing the pager prefix for this completion.
|
|
const SUPPRESS_PAGER_PREFIX = 1 << 10;
|
|
}
|
|
}
|
|
|
|
/// Function which accepts a completion string and returns its description.
|
|
pub type DescriptionFunc = Box<dyn Fn(&wstr) -> WString>;
|
|
|
|
/// Helper to return a [`DescriptionFunc`] for a constant string.
|
|
pub fn const_desc(s: &wstr) -> DescriptionFunc {
|
|
let s = s.to_owned();
|
|
Box::new(move |_| s.clone())
|
|
}
|
|
|
|
pub type CompletionList = Vec<Completion>;
|
|
|
|
/// This is an individual completion entry, i.e. the result of an expansion of a completion rule.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct Completion {
|
|
/// The completion string.
|
|
pub completion: WString,
|
|
/// The description for this completion.
|
|
pub description: WString,
|
|
/// The type of fuzzy match.
|
|
pub r#match: StringFuzzyMatch,
|
|
/// Flags determining the completion behavior.
|
|
pub flags: CompleteFlags,
|
|
}
|
|
|
|
impl Default for Completion {
|
|
fn default() -> Self {
|
|
Self {
|
|
completion: Default::default(),
|
|
description: Default::default(),
|
|
r#match: StringFuzzyMatch::exact_match(),
|
|
flags: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<WString> for Completion {
|
|
fn from(completion: WString) -> Completion {
|
|
Completion {
|
|
completion,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Completion {
|
|
pub fn new(
|
|
completion: WString,
|
|
description: WString,
|
|
r#match: StringFuzzyMatch, /* = exact_match */
|
|
flags: CompleteFlags,
|
|
) -> Self {
|
|
let flags = resolve_auto_space(&completion, flags);
|
|
Self {
|
|
completion,
|
|
description,
|
|
r#match,
|
|
flags,
|
|
}
|
|
}
|
|
|
|
pub fn from_completion(completion: WString) -> Self {
|
|
Self::with_desc(completion, WString::new())
|
|
}
|
|
|
|
pub fn with_desc(completion: WString, description: WString) -> Self {
|
|
Self::new(
|
|
completion,
|
|
description,
|
|
StringFuzzyMatch::exact_match(),
|
|
CompleteFlags::empty(),
|
|
)
|
|
}
|
|
|
|
/// Returns whether this replaces its token.
|
|
pub fn replaces_token(&self) -> bool {
|
|
self.flags.contains(CompleteFlags::REPLACES_TOKEN)
|
|
}
|
|
|
|
/// Returns whether this replaces the entire commandline.
|
|
pub fn replaces_line(&self) -> bool {
|
|
self.flags.contains(CompleteFlags::REPLACES_LINE)
|
|
}
|
|
|
|
/// Returns the completion's match rank. Lower ranks are better completions.
|
|
pub fn rank(&self) -> u32 {
|
|
self.r#match.rank()
|
|
}
|
|
|
|
/// If this completion replaces the entire token, prepend a prefix. Otherwise do nothing.
|
|
pub fn prepend_token_prefix(&mut self, prefix: &wstr) {
|
|
if self.replaces_token() {
|
|
self.completion.insert_utfstr(0, prefix);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CompletionRequestOptions {
|
|
/// Options for an autosuggestion.
|
|
pub fn autosuggest() -> Self {
|
|
Self {
|
|
autosuggestion: true,
|
|
descriptions: false,
|
|
fuzzy_match: false,
|
|
}
|
|
}
|
|
|
|
/// Options for a "normal" completion.
|
|
pub fn normal() -> Self {
|
|
Self {
|
|
autosuggestion: false,
|
|
descriptions: true,
|
|
fuzzy_match: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A completion receiver accepts completions. It is essentially a wrapper around `Vec` with
|
|
/// some conveniences.
|
|
pub struct CompletionReceiver {
|
|
/// Our list of completions.
|
|
completions: Vec<Completion>,
|
|
/// The maximum number of completions to add. If our list length exceeds this, then new
|
|
/// completions are not added. Note 0 has no special significance here - use
|
|
/// `usize::MAX` instead.
|
|
limit: usize,
|
|
}
|
|
|
|
// We are only wrapping a `Vec<Completion>`, any non-mutable methods can be safely deferred to the
|
|
// Vec-impl
|
|
impl Deref for CompletionReceiver {
|
|
type Target = [Completion];
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
self.completions.as_slice()
|
|
}
|
|
}
|
|
|
|
impl DerefMut for CompletionReceiver {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
self.completions.as_mut_slice()
|
|
}
|
|
}
|
|
|
|
impl CompletionReceiver {
|
|
/// Construct as empty, with a limit.
|
|
pub fn new(limit: usize) -> Self {
|
|
Self {
|
|
completions: vec![],
|
|
limit,
|
|
}
|
|
}
|
|
|
|
/// Acquire an existing list, with a limit.
|
|
pub fn from_list(completions: Vec<Completion>, limit: usize) -> Self {
|
|
Self { completions, limit }
|
|
}
|
|
|
|
/// Add a completion.
|
|
/// Return true on success, false if this would overflow the limit.
|
|
#[must_use]
|
|
pub fn add(&mut self, comp: impl Into<Completion>) -> bool {
|
|
if self.completions.len() >= self.limit {
|
|
return false;
|
|
}
|
|
self.completions.push(comp.into());
|
|
true
|
|
}
|
|
|
|
/// Adds a completion with the given string, and default other properties. Returns `true` on
|
|
/// success, `false` if this would overflow the limit.
|
|
#[must_use]
|
|
pub fn extend(
|
|
&mut self,
|
|
iter: impl IntoIterator<Item = Completion, IntoIter = impl ExactSizeIterator<Item = Completion>>,
|
|
) -> bool {
|
|
let iter = iter.into_iter();
|
|
if iter.len() > self.limit - self.completions.len() {
|
|
return false;
|
|
}
|
|
self.completions.extend(iter);
|
|
// this only fails if the ExactSizeIterator impl is bogus
|
|
assert!(
|
|
self.completions.len() <= self.limit,
|
|
"ExactSizeIterator returned more items than it should"
|
|
);
|
|
|
|
true
|
|
}
|
|
|
|
/// Clear the list of completions. This retains the storage inside `completions` which can be
|
|
/// useful to prevent allocations.
|
|
pub fn clear(&mut self) {
|
|
self.completions.clear();
|
|
}
|
|
|
|
/// Returns whether our completion list is empty.
|
|
pub fn empty(&self) -> bool {
|
|
self.completions.is_empty()
|
|
}
|
|
|
|
/// Returns how many completions we have stored.
|
|
pub fn size(&self) -> usize {
|
|
self.completions.len()
|
|
}
|
|
|
|
/// Returns the list of completions.
|
|
pub fn get_list(&self) -> &[Completion] {
|
|
&self.completions
|
|
}
|
|
|
|
/// Returns the list of completions.
|
|
pub fn get_list_mut(&mut self) -> &mut [Completion] {
|
|
&mut self.completions
|
|
}
|
|
|
|
/// Returns the list of completions, clearing it.
|
|
pub fn take(&mut self) -> Vec<Completion> {
|
|
std::mem::take(&mut self.completions)
|
|
}
|
|
|
|
/// Returns a new, empty receiver whose limit is our remaining capacity.
|
|
/// This is useful for e.g. recursive calls when you want to act on the result before adding it.
|
|
pub fn subreceiver(&self) -> Self {
|
|
let remaining_capacity = self
|
|
.limit
|
|
.checked_sub(self.completions.len())
|
|
.expect("length should never be larger than limit");
|
|
Self::new(remaining_capacity)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
pub enum CompleteOptionType {
|
|
/// no option
|
|
ArgsOnly,
|
|
/// `-x`
|
|
Short,
|
|
/// `-foo`
|
|
SingleLong,
|
|
/// `--foo`
|
|
DoubleLong,
|
|
}
|
|
|
|
/// Struct describing a completion rule for options to a command.
|
|
///
|
|
/// If option is empty, the comp field must not be empty and contains a list of arguments to the
|
|
/// command.
|
|
///
|
|
/// The type field determines how the option is to be interpreted: either empty (args_only) or
|
|
/// short, single-long ("old") or double-long ("GNU"). An invariant is that the option is empty if
|
|
/// and only if the type is args_only.
|
|
///
|
|
/// If option is non-empty, it specifies a switch for the command. If \c comp is also not empty, it
|
|
/// contains a list of non-switch arguments that may only follow directly after the specified
|
|
/// switch.
|
|
#[derive(Clone, Debug)]
|
|
struct CompleteEntryOpt {
|
|
/// Text of the option (like 'foo').
|
|
option: WString,
|
|
/// Arguments to the option; may be a subshell expression expanded at evaluation time.
|
|
comp: WString,
|
|
/// Description of the completion.
|
|
desc: LocalizableString,
|
|
/// Conditions under which to use the option, expanded and evaluated at completion time.
|
|
conditions: Vec<WString>,
|
|
/// Type of the option: `ArgsOnly`, `Short`, `SingleLong`, or `DoubleLong`.
|
|
typ: CompleteOptionType,
|
|
/// Determines how completions should be performed on the argument after the switch.
|
|
result_mode: CompletionMode,
|
|
/// Completion flags.
|
|
flags: CompleteFlags,
|
|
}
|
|
|
|
impl CompleteEntryOpt {
|
|
pub fn expected_dash_count(&self) -> usize {
|
|
match self.typ {
|
|
CompleteOptionType::ArgsOnly => 0,
|
|
CompleteOptionType::Short | CompleteOptionType::SingleLong => 1,
|
|
CompleteOptionType::DoubleLong => 2,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Last value used in the order field of [`CompletionEntry`].
|
|
static COMPLETE_ORDER: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
struct CompletionEntry {
|
|
/// List of all options.
|
|
options: Vec<CompleteEntryOpt>,
|
|
/// Order for when this completion was created. This aids in outputting completions sorted by
|
|
/// time.
|
|
order: usize,
|
|
}
|
|
|
|
impl CompletionEntry {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
options: vec![],
|
|
order: COMPLETE_ORDER.fetch_add(1, atomic::Ordering::Relaxed),
|
|
}
|
|
}
|
|
|
|
/// Getters for option list.
|
|
pub fn get_options(&self) -> &[CompleteEntryOpt] {
|
|
&self.options
|
|
}
|
|
|
|
/// Adds an option.
|
|
pub fn add_option(&mut self, opt: CompleteEntryOpt) {
|
|
self.options.push(opt);
|
|
}
|
|
|
|
/// Remove all completion options in the specified entry that match the specified short / long
|
|
/// option strings. Returns true if it is now empty and should be deleted, false if it's not
|
|
/// empty.
|
|
pub fn remove_option(&mut self, option: &wstr, typ: CompleteOptionType) -> bool {
|
|
self.options
|
|
.retain(|opt| opt.option != option || opt.typ != typ);
|
|
self.options.is_empty()
|
|
}
|
|
}
|
|
|
|
/// Set of all completion entries. Keyed by the command name, and whether it is a path.
|
|
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
|
struct CompletionEntryIndex {
|
|
name: WString,
|
|
is_path: bool,
|
|
}
|
|
type CompletionEntryMap = BTreeMap<CompletionEntryIndex, CompletionEntry>;
|
|
static COMPLETION_MAP: Mutex<CompletionEntryMap> = Mutex::new(BTreeMap::new());
|
|
static COMPLETION_TOMBSTONES: Mutex<BTreeSet<WString>> = Mutex::new(BTreeSet::new());
|
|
|
|
/// Completion "wrapper" support. The map goes from wrapping-command to wrapped-command-list.
|
|
type WrapperMap = HashMap<WString, Vec<WString>>;
|
|
static WRAPPER_MAP: LazyLock<Mutex<WrapperMap>> = LazyLock::new(|| Mutex::new(HashMap::new()));
|
|
|
|
/// Clear the [`CompleteFlags::AUTO_SPACE`] flag, and set [`CompleteFlags::NO_SPACE`] appropriately
|
|
/// depending on the suffix of the string.
|
|
fn resolve_auto_space(comp: &wstr, mut flags: CompleteFlags) -> CompleteFlags {
|
|
if flags.contains(CompleteFlags::AUTO_SPACE) {
|
|
flags -= CompleteFlags::AUTO_SPACE;
|
|
if let Some('/' | '=' | '@' | ':' | '.' | ',' | '-') = comp.as_char_slice().last() {
|
|
flags |= CompleteFlags::NO_SPACE;
|
|
}
|
|
}
|
|
|
|
flags
|
|
}
|
|
|
|
// If these functions aren't force inlined, it is actually faster to call
|
|
// stable_sort twice rather than to iterate once performing all comparisons in one go!
|
|
|
|
#[inline(always)]
|
|
fn natural_compare_completions(a: &Completion, b: &Completion) -> Ordering {
|
|
if (a.flags & b.flags).contains(CompleteFlags::DONT_SORT) {
|
|
// Both completions are from a source with the --keep-order flag.
|
|
return Ordering::Equal;
|
|
}
|
|
if (a.flags).contains(CompleteFlags::DONT_SORT) {
|
|
return Ordering::Less;
|
|
}
|
|
if (b.flags).contains(CompleteFlags::DONT_SORT) {
|
|
return Ordering::Greater;
|
|
}
|
|
wcsfilecmp(&a.completion, &b.completion)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn compare_completions_by_duplicate_arguments(a: &Completion, b: &Completion) -> Ordering {
|
|
let ad = a.flags.contains(CompleteFlags::DUPLICATES_ARGUMENT);
|
|
let bd = b.flags.contains(CompleteFlags::DUPLICATES_ARGUMENT);
|
|
|
|
ad.cmp(&bd)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn compare_completions_by_tilde(a: &Completion, b: &Completion) -> Ordering {
|
|
if a.completion.is_empty() || b.completion.is_empty() {
|
|
return Ordering::Equal;
|
|
}
|
|
|
|
let at = a.completion.ends_with('~');
|
|
let bt = b.completion.ends_with('~');
|
|
|
|
at.cmp(&bt)
|
|
}
|
|
|
|
/// Unique the list of completions, without perturbing their order.
|
|
fn unique_completions_retaining_order(comps: &mut Vec<Completion>) {
|
|
let mut seen = HashSet::with_capacity(comps.len());
|
|
let removals = comps.iter().enumerate().fold(vec![], |mut v, (i, c)| {
|
|
if !seen.insert(&c.completion) {
|
|
v.push(i);
|
|
}
|
|
v
|
|
});
|
|
|
|
// Remove in reverse order so the indexes remain valid
|
|
for idx in removals.iter().rev() {
|
|
comps.remove(*idx);
|
|
}
|
|
}
|
|
|
|
/// Sorts and removes any duplicate completions in the completion list, then puts them in priority
|
|
/// order.
|
|
pub fn sort_and_prioritize(comps: &mut Vec<Completion>, flags: CompletionRequestOptions) {
|
|
if comps.is_empty() {
|
|
return;
|
|
}
|
|
|
|
// Find the best rank.
|
|
let best_rank = comps.iter().map(Completion::rank).min().unwrap();
|
|
|
|
// Throw out completions of worse ranks.
|
|
comps.retain(|c| c.rank() == best_rank);
|
|
|
|
// Deduplicate both sorted and unsorted results.
|
|
unique_completions_retaining_order(comps);
|
|
|
|
// Sort, provided DONT_SORT isn't set.
|
|
// Here we do not pass suppress_exact, so that exact matches appear first.
|
|
comps.sort_by(natural_compare_completions);
|
|
|
|
// Lastly, if this is for an autosuggestion, prefer to avoid completions that duplicate
|
|
// arguments, and penalize files that end in tilde - they're frequently autosave files from e.g.
|
|
// emacs. Also prefer samecase to smartcase.
|
|
if flags.autosuggestion {
|
|
comps.sort_by(|a, b| {
|
|
a.r#match
|
|
.case_fold
|
|
.cmp(&b.r#match.case_fold)
|
|
.then_with(|| compare_completions_by_duplicate_arguments(a, b))
|
|
.then_with(|| compare_completions_by_tilde(a, b))
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Bag of data to support expanding a command's arguments using custom completions, including
|
|
/// the wrap chain.
|
|
struct CustomArgData<'a> {
|
|
/// The unescaped argument before the argument which is being completed, or empty if none.
|
|
previous_argument: WString,
|
|
/// The unescaped argument which is being completed, or empty if none.
|
|
current_argument: WString,
|
|
/// Whether a -- has been encountered, which suppresses options.
|
|
had_ddash: bool,
|
|
/// Whether to perform file completions.
|
|
/// This is an "out" parameter of the wrap chain walk: if any wrapped command suppresses file
|
|
/// completions this gets set to false.
|
|
do_file: bool,
|
|
/// Depth in the wrap chain.
|
|
wrap_depth: usize,
|
|
/// The list of variable assignments: escaped strings of the form VAR=VAL.
|
|
/// This may be temporarily appended to as we explore the wrap chain.
|
|
/// When completing, variable assignments are really set in a local scope.
|
|
var_assignments: &'a mut Vec<WString>,
|
|
/// The set of wrapped commands which we have visited, and so should not be explored again.
|
|
visited_wrapped_commands: HashSet<WString>,
|
|
}
|
|
|
|
impl<'a> CustomArgData<'a> {
|
|
pub fn new(var_assignments: &'a mut Vec<WString>) -> Self {
|
|
Self {
|
|
previous_argument: WString::new(),
|
|
current_argument: WString::new(),
|
|
had_ddash: false,
|
|
do_file: true,
|
|
wrap_depth: 0,
|
|
var_assignments,
|
|
visited_wrapped_commands: HashSet::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Class representing an attempt to compute completions.
|
|
struct Completer<'ctx> {
|
|
/// The operation context for this completion.
|
|
ctx: &'ctx OperationContext<'ctx>,
|
|
/// Flags associated with the completion request.
|
|
flags: CompletionRequestOptions,
|
|
/// The output completions.
|
|
completions: CompletionReceiver,
|
|
/// Commands which we would have tried to load, if we had a parser.
|
|
needs_load: Vec<WString>,
|
|
/// Table of completions conditions that have already been tested and the corresponding test
|
|
/// results.
|
|
condition_cache: HashMap<WString, bool>,
|
|
}
|
|
|
|
static COMPLETION_AUTOLOADER: LazyLock<Mutex<Autoload>> =
|
|
LazyLock::new(|| Mutex::new(Autoload::new(L!("fish_complete_path"))));
|
|
|
|
impl<'ctx> Completer<'ctx> {
|
|
pub fn new(ctx: &'ctx OperationContext<'ctx>, flags: CompletionRequestOptions) -> Self {
|
|
Self {
|
|
ctx,
|
|
flags,
|
|
completions: CompletionReceiver::new(ctx.expansion_limit),
|
|
needs_load: vec![],
|
|
condition_cache: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
fn perform_for_commandline(&mut self, cmdline: WString) {
|
|
// Limit recursion, in case a user-defined completion has cycles, or the completion for "x"
|
|
// wraps "A=B x" (#3474, #7344). No need to do that when there is no parser: this happens only
|
|
// for autosuggestions where we don't evaluate command substitutions or variable assignments.
|
|
if let Some(parser) = self.ctx.maybe_parser() {
|
|
let level = &mut parser.libdata_mut().complete_recursion_level;
|
|
if *level >= 24 {
|
|
flog!(
|
|
error,
|
|
wgettext!("completion reached maximum recursion depth, possible cycle?"),
|
|
);
|
|
return;
|
|
}
|
|
*level += 1;
|
|
}
|
|
self.perform_for_commandline_impl(cmdline);
|
|
if let Some(parser) = self.ctx.maybe_parser() {
|
|
parser.libdata_mut().complete_recursion_level -= 1;
|
|
}
|
|
}
|
|
|
|
fn perform_for_commandline_impl(&mut self, cmdline: WString) {
|
|
let cursor_pos = cmdline.len();
|
|
let is_autosuggest = self.flags.autosuggestion;
|
|
|
|
// Find the process to operate on. The cursor may be past it (#1261), so backtrack
|
|
// until we know we're no longer in a space. But the space may actually be part of the
|
|
// argument (#2477).
|
|
let mut position_in_statement = cursor_pos;
|
|
while position_in_statement > 0 && cmdline.char_at(position_in_statement - 1) == ' ' {
|
|
position_in_statement -= 1;
|
|
}
|
|
|
|
// Get all the arguments.
|
|
let mut tokens = Vec::new();
|
|
get_process_extent(&cmdline, position_in_statement, Some(&mut tokens));
|
|
let actual_token_count = tokens.len();
|
|
|
|
// Hack: fix autosuggestion by removing prefixing "and"s #6249.
|
|
if is_autosuggest {
|
|
let prefixed_supercommand_count = tokens
|
|
.iter()
|
|
.take_while(|token| {
|
|
parser_keywords_is_subcommand(&unescape_keyword(
|
|
token.type_,
|
|
token.get_source(&cmdline),
|
|
))
|
|
})
|
|
.count();
|
|
tokens.drain(..prefixed_supercommand_count);
|
|
}
|
|
|
|
// Consume variable assignments in tokens strictly before the cursor.
|
|
// This is a list of (escaped) strings of the form VAR=VAL.
|
|
// TODO: filter_drain
|
|
let mut var_assignments = Vec::new();
|
|
for tok in &tokens {
|
|
if tok.location_in_or_at_end_of_source_range(cursor_pos) {
|
|
break;
|
|
}
|
|
let tok_src = tok.get_source(&cmdline);
|
|
if variable_assignment_equals_pos(tok_src).is_none() {
|
|
break;
|
|
}
|
|
var_assignments.push(tok_src.to_owned());
|
|
}
|
|
tokens.drain(..var_assignments.len());
|
|
|
|
// Empty process (cursor is after one of ;, &, |, \n, &&, || modulo whitespace).
|
|
let [first_token, ..] = tokens.as_slice() else {
|
|
// Don't autosuggest anything based on the empty string (generalizes #1631).
|
|
if is_autosuggest {
|
|
return;
|
|
}
|
|
self.complete_cmd(WString::new());
|
|
self.complete_abbr(WString::new());
|
|
return;
|
|
};
|
|
|
|
let effective_cmdline = if tokens.len() == actual_token_count {
|
|
&cmdline
|
|
} else {
|
|
&cmdline[first_token.offset()..]
|
|
};
|
|
|
|
if tokens.last().unwrap().type_ == TokenType::Comment {
|
|
return;
|
|
}
|
|
tokens.retain(|tok| tok.type_ != TokenType::Comment);
|
|
assert!(!tokens.is_empty());
|
|
|
|
let cmd_tok = tokens.first().unwrap();
|
|
let cur_tok = tokens.last().unwrap();
|
|
|
|
// Since fish does not currently support redirect in command position, we return here.
|
|
if cmd_tok.type_ != TokenType::String {
|
|
return;
|
|
}
|
|
if cur_tok.type_ == TokenType::Error {
|
|
return;
|
|
}
|
|
for tok in &tokens {
|
|
// If there was an error, it was in the last token.
|
|
assert_matches!(tok.type_, TokenType::String | TokenType::Redirect);
|
|
}
|
|
// If we are completing a variable name or a tilde expansion user name, we do that and
|
|
// return. No need for any other completions.
|
|
let current_token = cur_tok.get_source(&cmdline);
|
|
if cur_tok.location_in_or_at_end_of_source_range(cursor_pos)
|
|
&& (self.try_complete_variable(current_token) || self.try_complete_user(current_token))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if cmd_tok.location_in_or_at_end_of_source_range(cursor_pos) {
|
|
let equals_sign_pos = variable_assignment_equals_pos(current_token);
|
|
if let Some(pos) = equals_sign_pos {
|
|
let first = self.completions.len();
|
|
self.complete_param_expand(
|
|
¤t_token[pos + 1..],
|
|
/*do_file=*/ true,
|
|
/*handle_as_special_cd=*/ false,
|
|
cur_tok.is_unterminated_brace,
|
|
);
|
|
for c in &mut self.completions[first..] {
|
|
c.flags |= CompleteFlags::KEEP_VARIABLE_OVERRIDE_PREFIX;
|
|
}
|
|
return;
|
|
}
|
|
// Complete command filename.
|
|
let current_token = current_token.to_owned();
|
|
self.complete_abbr(current_token.clone());
|
|
self.complete_cmd(current_token);
|
|
return;
|
|
}
|
|
// See whether we are in an argument, in a redirection or in the whitespace in between.
|
|
let mut in_redirection = cur_tok.type_ == TokenType::Redirect;
|
|
|
|
let mut had_ddash = false;
|
|
let mut current_argument = L!("");
|
|
let mut previous_argument = L!("");
|
|
if cur_tok.type_ == TokenType::String
|
|
&& cur_tok.location_in_or_at_end_of_source_range(position_in_statement)
|
|
{
|
|
// If the cursor is in whitespace, then the "current" argument is empty and the
|
|
// previous argument is the matching one. But if the cursor was in or at the end
|
|
// of the argument, then the current argument is the matching one, and the
|
|
// previous argument is the one before it.
|
|
let cursor_in_whitespace = !cur_tok.location_in_or_at_end_of_source_range(cursor_pos);
|
|
if cursor_in_whitespace {
|
|
previous_argument = current_token;
|
|
} else {
|
|
current_argument = current_token;
|
|
if tokens.len() >= 2 {
|
|
let prev_tok = &tokens[tokens.len() - 2];
|
|
if prev_tok.type_ == TokenType::String {
|
|
previous_argument = prev_tok.get_source(&cmdline);
|
|
}
|
|
in_redirection = prev_tok.type_ == TokenType::Redirect;
|
|
}
|
|
}
|
|
|
|
// Check to see if we have a preceding double-dash.
|
|
for tok in &tokens[..tokens.len() - 1] {
|
|
if tok.get_source(&cmdline) == "--" {
|
|
had_ddash = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut do_file = false;
|
|
let mut handle_as_special_cd = false;
|
|
if in_redirection {
|
|
do_file = true;
|
|
} else {
|
|
// Try completing as an argument.
|
|
let mut arg_data = CustomArgData::new(&mut var_assignments);
|
|
arg_data.had_ddash = had_ddash;
|
|
|
|
let bias = cmdline.len() - effective_cmdline.len();
|
|
let command_range = SourceRange::new(cmd_tok.offset() - bias, cmd_tok.length());
|
|
|
|
let mut exp_command = cmd_tok.get_source(&cmdline).to_owned();
|
|
let mut prev = None;
|
|
let mut cur = None;
|
|
if expand_command_token(self.ctx, &mut exp_command) {
|
|
prev = unescape_string(previous_argument, UnescapeStringStyle::default());
|
|
cur = unescape_string(
|
|
current_argument,
|
|
UnescapeStringStyle::Script(UnescapeFlags::INCOMPLETE),
|
|
);
|
|
}
|
|
if let (Some(prev), Some(cur)) = (prev, cur) {
|
|
arg_data.previous_argument = prev;
|
|
arg_data.current_argument = cur;
|
|
// Have to walk over the command and its entire wrap chain. If any command
|
|
// disables do_file, then they all do.
|
|
self.walk_wrap_chain(
|
|
&exp_command,
|
|
effective_cmdline,
|
|
command_range,
|
|
&mut arg_data,
|
|
);
|
|
do_file = arg_data.do_file;
|
|
|
|
// If we're autosuggesting, and the token is empty, don't do file suggestions.
|
|
if is_autosuggest && arg_data.current_argument.is_empty() {
|
|
do_file = false;
|
|
}
|
|
}
|
|
|
|
// Hack. If we're cd, handle it specially (issue #1059, others).
|
|
handle_as_special_cd =
|
|
exp_command == "cd" || arg_data.visited_wrapped_commands.contains(L!("cd"));
|
|
}
|
|
|
|
// Maybe apply variable assignments.
|
|
let _restore_vars = self.apply_var_assignments(&var_assignments);
|
|
if self.ctx.check_cancel() {
|
|
return;
|
|
}
|
|
|
|
// This function wants the unescaped string.
|
|
self.complete_param_expand(
|
|
current_argument,
|
|
do_file,
|
|
handle_as_special_cd,
|
|
cur_tok.is_unterminated_brace,
|
|
);
|
|
|
|
// Lastly mark any completions that appear to already be present in arguments.
|
|
self.mark_completions_duplicating_arguments(&cmdline, current_token, tokens);
|
|
}
|
|
|
|
pub fn acquire_completions(&mut self) -> Vec<Completion> {
|
|
self.completions.take()
|
|
}
|
|
|
|
pub fn acquire_needs_load(&mut self) -> Vec<WString> {
|
|
mem::take(&mut self.needs_load)
|
|
}
|
|
|
|
/// Test if the specified script returns zero. The result is cached, so that if multiple completions
|
|
/// use the same condition, it needs only be evaluated once. condition_cache_clear must be called
|
|
/// after a completion run to make sure that there are no stale completions.
|
|
fn condition_test(&mut self, condition: &wstr) -> bool {
|
|
if condition.is_empty() {
|
|
return true;
|
|
}
|
|
let Some(parser) = self.ctx.maybe_parser() else {
|
|
return false;
|
|
};
|
|
|
|
let cached_entry = self.condition_cache.get(condition);
|
|
if let Some(&entry) = cached_entry {
|
|
// Use the old value.
|
|
entry
|
|
} else {
|
|
// Compute new value and reinsert it.
|
|
let test_res = exec_subshell(
|
|
condition, parser, None, false, /* don't apply exit status */
|
|
)
|
|
.is_ok();
|
|
self.condition_cache.insert(condition.to_owned(), test_res);
|
|
test_res
|
|
}
|
|
}
|
|
|
|
fn conditions_test(&mut self, conditions: &[WString]) -> bool {
|
|
conditions.iter().all(|c| self.condition_test(c))
|
|
}
|
|
|
|
/// Copy any strings in `possible_comp` which have the specified prefix to the
|
|
/// completer's completion array. The prefix may contain wildcards. The output
|
|
/// will consist of [`Completion`] structs.
|
|
///
|
|
/// There are three ways to specify descriptions for each completion. Firstly,
|
|
/// if a description has already been added to the completion, it is _not_
|
|
/// replaced. Secondly, if the `desc_func` function is specified, use it to
|
|
/// determine a dynamic completion. Thirdly, if none of the above are available,
|
|
/// the `desc` string is used as a description.
|
|
///
|
|
/// - `wc_escaped`: the prefix, possibly containing wildcards. The wildcard should not have
|
|
/// been unescaped, i.e. '*' should be used for any string, not the `ANY_STRING` character.
|
|
/// - `desc_func`: the function that generates a description for those completions without an
|
|
/// embedded description
|
|
/// - `possible_comp`: the list of possible completions to iterate over
|
|
/// - `flags`: The flags controlling completion
|
|
/// - `extra_expand_flags`: Additional flags controlling expansion.
|
|
fn complete_strings(
|
|
&mut self,
|
|
wc_escaped: &wstr,
|
|
desc_func: &DescriptionFunc,
|
|
possible_comp: &[Completion],
|
|
flags: CompleteFlags,
|
|
extra_expand_flags: ExpandFlags,
|
|
) {
|
|
let mut tmp = wc_escaped.to_owned();
|
|
if !expand_one(
|
|
&mut tmp,
|
|
self.expand_flags()
|
|
| extra_expand_flags
|
|
| ExpandFlags::FAIL_ON_CMDSUBST
|
|
| ExpandFlags::SKIP_WILDCARDS,
|
|
self.ctx,
|
|
None,
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let wc = unescape_wildcards(&tmp);
|
|
for comp in possible_comp {
|
|
let comp_str = &comp.completion;
|
|
if !comp_str.is_empty() {
|
|
let expand_flags = self.expand_flags() | extra_expand_flags;
|
|
wildcard_complete(
|
|
comp_str,
|
|
&wc,
|
|
Some(desc_func),
|
|
Some(&mut self.completions),
|
|
expand_flags,
|
|
flags,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn expand_flags(&self) -> ExpandFlags {
|
|
let mut result = ExpandFlags::empty();
|
|
result.set(ExpandFlags::FAIL_ON_CMDSUBST, self.flags.autosuggestion);
|
|
result.set(ExpandFlags::FUZZY_MATCH, self.flags.fuzzy_match);
|
|
result.set(ExpandFlags::GEN_DESCRIPTIONS, self.flags.descriptions);
|
|
result
|
|
}
|
|
|
|
/// If command to complete is short enough, substitute the description with the whatis information
|
|
/// for the executable.
|
|
fn complete_cmd_desc(&mut self, s: &wstr) {
|
|
let Some(parser) = self.ctx.maybe_parser() else {
|
|
return;
|
|
};
|
|
|
|
let cmd = if let Some(pos) = s.chars().rposition(|c| c == '/') {
|
|
if pos + 1 > s.len() {
|
|
return;
|
|
}
|
|
&s[pos + 1..]
|
|
} else {
|
|
s
|
|
};
|
|
|
|
// Using apropos with a single-character search term produces far too many results - require at
|
|
// least two characters if we don't know the location of the whatis-database.
|
|
if cmd.len() < 2 {
|
|
return;
|
|
}
|
|
|
|
if wildcard_has(cmd) {
|
|
return;
|
|
}
|
|
|
|
let keep_going =
|
|
self.completions.get_list().iter().any(|c| {
|
|
c.completion.is_empty() || c.completion.as_char_slice().last() != Some(&'/')
|
|
});
|
|
if !keep_going {
|
|
return;
|
|
}
|
|
|
|
// On Cygwin, if `cmd` contains part of the `.exe` extension (e.g. `lsmod.e`), we are unlikely
|
|
// to find a description since they are usually associated to the POSIX name (`lsmod`). So we also
|
|
// need to search for the stripped command (`lsmod`), and later associate the description to
|
|
// the missing part of the extension (`xe`)
|
|
let no_exe = strip_partial_executable_suffix(cmd);
|
|
|
|
// First locate a list of possible descriptions using a single call to apropos or a direct
|
|
// search if we know the location of the whatis database. This can take some time on slower
|
|
// systems with a large set of manuals, but it should be ok since apropos is only called once.
|
|
// For Cygwin, also try to find the exact match for the non-exe name
|
|
let lookup_cmd = sprintf!(
|
|
"functions -q __fish_describe_command &&{ __fish_describe_command -- %s %s}",
|
|
&escape(cmd),
|
|
&no_exe
|
|
.map(|(cmd_sans_exe, _)| {
|
|
sprintf!(
|
|
"; __fish_describe_command --exact -- %s",
|
|
escape(cmd_sans_exe)
|
|
)
|
|
})
|
|
.unwrap_or_default()[..]
|
|
);
|
|
|
|
let mut list = vec![];
|
|
let _ = exec_subshell(
|
|
&lookup_cmd,
|
|
parser,
|
|
Some(&mut list),
|
|
false, /* don't apply exit status */
|
|
);
|
|
|
|
// Then discard anything that is not a possible completion and put the result into a
|
|
// hashtable with the completion as key and the description as value.
|
|
let mut lookup = BTreeMap::new();
|
|
// A typical entry is the command name, followed by a tab, followed by a description.
|
|
for elstr in &mut list {
|
|
// Skip cases without a tab, or without a description
|
|
// Bizarre cases where the tab is part of the command will be filtered later.
|
|
let Some(tab_idx) = elstr.find_char('\t') else {
|
|
continue;
|
|
};
|
|
if tab_idx + 1 >= elstr.len() {
|
|
continue;
|
|
}
|
|
|
|
// Make the set components. This is the stuff after the command.
|
|
// For example:
|
|
// elstr = lsmod\ta description
|
|
// cmd = ls
|
|
// key = mod
|
|
// val = A description
|
|
// Note an empty key is common and natural, if 'cmd' were already valid.
|
|
let parts = elstr.as_mut_utfstr().split_at_mut(tab_idx);
|
|
let key = if parts.0.len() >= cmd.len() {
|
|
&parts.0[cmd.len()..]
|
|
} else if let Some((_, comp)) = no_exe.filter(|(stripped, _)| stripped == parts.0) {
|
|
// On Cygwin, `cmd` might be `lsmod.e`, then key needs to be `xe`, while
|
|
// elstr is `lsmod\t...` (i.e. parts.0 is `lsmod`)
|
|
comp
|
|
} else {
|
|
continue;
|
|
};
|
|
let val = &mut parts.1[1..];
|
|
|
|
// And once again I make sure the first character is uppercased because I like it that
|
|
// way, and I get to decide these things.
|
|
let mut upper_chars = val.chars().next().unwrap().to_uppercase();
|
|
if let (Some(c), None) = (upper_chars.next(), upper_chars.next()) {
|
|
val.as_char_slice_mut()[0] = c;
|
|
}
|
|
lookup.insert(key, &*val);
|
|
}
|
|
|
|
// Then do a lookup on every completion and if a match is found, change to the new
|
|
// description.
|
|
for completion in self.completions.get_list_mut() {
|
|
let el = &completion.completion;
|
|
if let Some(&desc) = lookup.get(el.as_utfstr()) {
|
|
completion.description = desc.to_owned();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Complete the specified command name. Search for executables in the path, executables defined
|
|
/// using an absolute path, functions, builtins and directories for implicit cd commands.
|
|
///
|
|
/// \param str_cmd the command string to find completions for
|
|
fn complete_cmd(&mut self, str_cmd: WString) {
|
|
// Append all possible executables
|
|
let result = {
|
|
let expand_flags = self.expand_flags()
|
|
| ExpandFlags::SPECIAL_FOR_COMMAND
|
|
| ExpandFlags::FOR_COMPLETIONS
|
|
| ExpandFlags::PRESERVE_HOME_TILDES
|
|
| ExpandFlags::EXECUTABLES_ONLY;
|
|
expand_to_receiver(
|
|
str_cmd.clone(),
|
|
&mut self.completions,
|
|
expand_flags,
|
|
self.ctx,
|
|
None,
|
|
)
|
|
.result
|
|
};
|
|
if result == ExpandResultCode::cancel {
|
|
return;
|
|
}
|
|
if result == ExpandResultCode::ok && self.flags.descriptions {
|
|
self.complete_cmd_desc(&str_cmd);
|
|
}
|
|
|
|
// We don't really care if this succeeds or fails. If it succeeds this->completions will be
|
|
// updated with choices for the user.
|
|
let _ = {
|
|
// Append all matching directories
|
|
let expand_flags = self.expand_flags()
|
|
| ExpandFlags::FOR_COMPLETIONS
|
|
| ExpandFlags::PRESERVE_HOME_TILDES
|
|
| ExpandFlags::DIRECTORIES_ONLY;
|
|
expand_to_receiver(
|
|
str_cmd.clone(),
|
|
&mut self.completions,
|
|
expand_flags,
|
|
self.ctx,
|
|
None,
|
|
)
|
|
};
|
|
|
|
if str_cmd.is_empty() || (!str_cmd.contains('/') && str_cmd.as_char_slice()[0] != '~') {
|
|
let include_hidden = str_cmd.as_char_slice().first() == Some(&'_');
|
|
// Append all known matching functions
|
|
let possible_comp: Vec<_> = function::get_names(include_hidden, self.ctx.vars())
|
|
.into_iter()
|
|
.map(Completion::from_completion)
|
|
.collect();
|
|
|
|
self.complete_strings(
|
|
&str_cmd,
|
|
&{ Box::new(complete_function_desc) as DescriptionFunc },
|
|
&possible_comp,
|
|
CompleteFlags::empty(),
|
|
ExpandFlags::empty(),
|
|
);
|
|
|
|
// Append all matching builtins
|
|
let possible_comp: Vec<_> = builtin_get_names()
|
|
.map(wstr::to_owned)
|
|
.map(Completion::from_completion)
|
|
.collect();
|
|
|
|
self.complete_strings(
|
|
&str_cmd,
|
|
&{ Box::new(|name| builtin_get_desc(name).unwrap_or(L!("")).to_owned()) },
|
|
&possible_comp,
|
|
CompleteFlags::empty(),
|
|
ExpandFlags::empty(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Attempt to complete an abbreviation for the given string.
|
|
fn complete_abbr(&mut self, cmd: WString) {
|
|
// Copy the list of names and descriptions so as not to hold the lock across the call to
|
|
// complete_strings.
|
|
let mut possible_comp = Vec::new();
|
|
let mut descs = HashMap::new();
|
|
with_abbrs(|set| {
|
|
for abbr in set.list() {
|
|
if !abbr.is_regex() {
|
|
possible_comp.push(Completion::from_completion(abbr.key.clone()));
|
|
descs.insert(abbr.key.clone(), abbr.replacement.clone());
|
|
}
|
|
}
|
|
});
|
|
|
|
let desc_func = move |key: &wstr| {
|
|
let replacement = descs.get(key).expect("Abbreviation not found");
|
|
wgettext_fmt!(ABBR_DESC, replacement)
|
|
};
|
|
self.complete_strings(
|
|
&cmd,
|
|
&{ Box::new(desc_func) as _ },
|
|
&possible_comp,
|
|
CompleteFlags::NO_SPACE,
|
|
ExpandFlags::empty(),
|
|
);
|
|
}
|
|
|
|
/// Evaluate the argument list (as supplied by `complete -a`) and insert any
|
|
/// return matching completions. Matching is done using `copy_strings_with_prefix`,
|
|
/// meaning the completion may contain wildcards.
|
|
/// Logically, this is not always the right thing to do, but I have yet to come
|
|
/// up with a case where this matters.
|
|
///
|
|
/// - `str`: The string to complete.
|
|
/// - `args`: The list of option arguments to be evaluated.
|
|
/// - `desc`: Description of the completion
|
|
/// - `flags`: The flags
|
|
fn complete_from_args(&mut self, s: &wstr, args: &wstr, desc: &wstr, flags: CompleteFlags) {
|
|
let is_autosuggest = self.flags.autosuggestion;
|
|
|
|
let mut saved_statuses = None;
|
|
let mut scope = None;
|
|
if let Some(parser) = self.ctx.maybe_parser() {
|
|
saved_statuses = Some(parser.get_last_statuses());
|
|
scope = Some(parser.push_scope(|s| s.is_interactive = false));
|
|
}
|
|
|
|
let eflags = if is_autosuggest {
|
|
ExpandFlags::FAIL_ON_CMDSUBST
|
|
} else {
|
|
ExpandFlags::empty()
|
|
};
|
|
|
|
let possible_comp = Parser::expand_argument_list(args, eflags, self.ctx);
|
|
|
|
if let Some(parser) = self.ctx.maybe_parser() {
|
|
parser.set_last_statuses(saved_statuses.unwrap());
|
|
}
|
|
std::mem::drop(scope);
|
|
|
|
// Allow leading dots - see #3707.
|
|
self.complete_strings(
|
|
&escape(s),
|
|
&const_desc(desc),
|
|
&possible_comp,
|
|
flags,
|
|
ExpandFlags::ALLOW_NONLITERAL_LEADING_DOT,
|
|
);
|
|
}
|
|
|
|
/// complete_param: Given a command, find completions for the argument `s` of command `cmd_orig`
|
|
/// with previous option `popt`. If file completions should be disabled, then mark
|
|
/// `out_do_file` as `false`.
|
|
///
|
|
/// Returns `true` if successful, `false` if there's an error.
|
|
///
|
|
/// Examples in format (cmd, popt, str):
|
|
///
|
|
/// ```text
|
|
/// echo hello world <tab> -> ("echo", "world", "")
|
|
/// echo hello world<tab> -> ("echo", "hello", "world")
|
|
/// ```
|
|
fn complete_param_for_command(
|
|
&mut self,
|
|
cmd_orig: &wstr,
|
|
popt: &wstr,
|
|
s: &wstr,
|
|
use_switches: bool,
|
|
out_do_file: &mut bool,
|
|
) -> bool {
|
|
let mut use_files = true;
|
|
let mut has_force = false;
|
|
|
|
let CmdString { cmd, path } = parse_cmd_string(cmd_orig, self.ctx.vars());
|
|
|
|
// Don't use cmd_orig here for paths. It's potentially pathed,
|
|
// so that command might exist, but the completion script
|
|
// won't be using it.
|
|
let cmd_exists = builtin_exists(&cmd)
|
|
|| function::exists_no_autoload(&cmd)
|
|
|| path_get_path(&cmd, self.ctx.vars()).is_some();
|
|
if !cmd_exists {
|
|
// Do not load custom completions if the command does not exist
|
|
// This prevents errors caused during the execution of completion providers for
|
|
// tools that do not exist. Applies to both manual completions ("cm<TAB>", "cmd <TAB>")
|
|
// and automatic completions ("gi" autosuggestion provider -> git)
|
|
flog!(complete, "Skipping completions for non-existent command");
|
|
} else if let Some(parser) = self.ctx.maybe_parser() {
|
|
complete_load(&cmd, parser);
|
|
} else if !COMPLETION_AUTOLOADER
|
|
.lock()
|
|
.unwrap()
|
|
.has_attempted_autoload(&cmd)
|
|
{
|
|
self.needs_load.push(cmd.clone());
|
|
}
|
|
|
|
// Make a list of lists of all options that we care about.
|
|
let all_options: Vec<Vec<CompleteEntryOpt>> = COMPLETION_MAP
|
|
.lock()
|
|
.unwrap()
|
|
.iter()
|
|
.filter_map(|(idx, completion)| {
|
|
let r#match = if idx.is_path { &path } else { &cmd };
|
|
let has_match = wildcard_match(r#match, &idx.name, false)
|
|
|| (
|
|
// On cygwin, if we didn't have a completion for "foo.exe",
|
|
// check if there is one for "foo"
|
|
!idx.is_path
|
|
&& strip_executable_suffix(r#match)
|
|
.is_some_and(|stripped| wildcard_match(stripped, &idx.name, false))
|
|
);
|
|
if has_match {
|
|
// Copy all of their options into our list. Oof, this is a lot of copying.
|
|
let mut options = completion.get_options().to_vec();
|
|
// We have to copy them in reverse order to preserve legacy behavior (#9221).
|
|
options.reverse();
|
|
Some(options)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
// Now release the lock and test each option that we captured above. We have to do this outside
|
|
// the lock because callouts (like the condition) may add or remove completions. See issue #2.
|
|
for options in all_options {
|
|
let short_opt_pos = short_option_pos(s, &options);
|
|
// We want last_option_requires_param to default to false but distinguish between when
|
|
// a previous completion has set it to false and when it has its default value.
|
|
let mut last_option_requires_param = None;
|
|
let mut use_common = true;
|
|
if use_switches {
|
|
if s.char_at(0) == '-' {
|
|
// Check if we are entering a combined option and argument (like --color=auto or
|
|
// -I/usr/include).
|
|
for o in &options {
|
|
let arg_offset = if o.typ == CompleteOptionType::Short {
|
|
let Some(short_opt_pos) = short_opt_pos else {
|
|
continue;
|
|
};
|
|
if o.option.char_at(0) != s.char_at(short_opt_pos) {
|
|
continue;
|
|
}
|
|
Some(short_opt_pos + 1)
|
|
} else {
|
|
param_match2(o, s)
|
|
};
|
|
|
|
if self.conditions_test(&o.conditions) {
|
|
if o.typ == CompleteOptionType::Short {
|
|
// Only override a true last_option_requires_param value with a false
|
|
// one
|
|
*last_option_requires_param
|
|
.get_or_insert(o.result_mode.requires_param) &=
|
|
o.result_mode.requires_param;
|
|
}
|
|
if let Some(arg_offset) = arg_offset {
|
|
if o.result_mode.requires_param {
|
|
use_common = false;
|
|
}
|
|
if o.result_mode.no_files {
|
|
use_files = false;
|
|
}
|
|
if o.result_mode.force_files {
|
|
has_force = true;
|
|
}
|
|
let (arg_prefix, arg) = s.split_once(arg_offset);
|
|
let first_new = self.completions.completions.len();
|
|
self.complete_from_args(arg, &o.comp, o.desc.localize(), o.flags);
|
|
for compl in &mut self.completions.completions[first_new..] {
|
|
if compl.replaces_token() {
|
|
compl.completion.insert_utfstr(0, arg_prefix);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if popt.char_at(0) == '-' {
|
|
// Set to true if we found a matching old-style switch.
|
|
// Here we are testing the previous argument,
|
|
// to see how we should complete the current argument
|
|
let mut old_style_match = false;
|
|
|
|
// If we are using old style long options, check for them first.
|
|
for o in &options {
|
|
if o.typ == CompleteOptionType::SingleLong
|
|
&& param_match(o, popt)
|
|
&& self.conditions_test(&o.conditions)
|
|
{
|
|
old_style_match = false;
|
|
if o.result_mode.requires_param {
|
|
use_common = false;
|
|
}
|
|
if o.result_mode.no_files {
|
|
use_files = false;
|
|
}
|
|
if o.result_mode.force_files {
|
|
has_force = true;
|
|
}
|
|
self.complete_from_args(s, &o.comp, o.desc.localize(), o.flags);
|
|
}
|
|
}
|
|
|
|
// No old style option matched, or we are not using old style options. We check if
|
|
// any short (or gnu style) options do.
|
|
if !old_style_match {
|
|
let prev_short_opt_pos = short_option_pos(popt, &options);
|
|
for o in &options {
|
|
// Gnu-style options with _optional_ arguments must be specified as a single
|
|
// token, so that it can be differed from a regular argument.
|
|
// Here we are testing the previous argument for a GNU-style match,
|
|
// to see how we should complete the current argument
|
|
if !o.result_mode.requires_param {
|
|
continue;
|
|
}
|
|
|
|
let mut r#match = false;
|
|
if o.typ == CompleteOptionType::Short {
|
|
if let Some(prev_short_opt_pos) = prev_short_opt_pos {
|
|
r#match = prev_short_opt_pos + 1 == popt.len()
|
|
&& o.option.char_at(0) == popt.char_at(prev_short_opt_pos);
|
|
}
|
|
} else if o.typ == CompleteOptionType::DoubleLong {
|
|
r#match = param_match(o, popt);
|
|
}
|
|
if r#match && self.conditions_test(&o.conditions) {
|
|
if o.result_mode.requires_param {
|
|
use_common = false;
|
|
}
|
|
if o.result_mode.no_files {
|
|
use_files = false;
|
|
}
|
|
if o.result_mode.force_files {
|
|
has_force = true;
|
|
}
|
|
self.complete_from_args(s, &o.comp, o.desc.localize(), o.flags);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !use_common {
|
|
continue;
|
|
}
|
|
|
|
// Set a default value for last_option_requires_param only if one hasn't been set
|
|
let last_option_requires_param = last_option_requires_param.unwrap_or(false);
|
|
|
|
// Now we try to complete an option itself
|
|
for o in &options {
|
|
// If this entry is for the base command, check if any of the arguments match.
|
|
if !self.conditions_test(&o.conditions) {
|
|
continue;
|
|
}
|
|
if o.option.is_empty() {
|
|
use_files &= !o.result_mode.no_files;
|
|
has_force |= o.result_mode.force_files;
|
|
self.complete_from_args(s, &o.comp, o.desc.localize(), o.flags);
|
|
}
|
|
|
|
if !use_switches || s.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
// Check if the short style option matches.
|
|
if o.typ == CompleteOptionType::Short {
|
|
let optchar = o.option.char_at(0);
|
|
if let Some(short_opt_pos) = short_opt_pos {
|
|
// Only complete when the last short option has no parameter yet..
|
|
if short_opt_pos + 1 != s.len() {
|
|
continue;
|
|
}
|
|
// .. and it does not require one ..
|
|
if last_option_requires_param {
|
|
continue;
|
|
}
|
|
// .. and the option is not already there.
|
|
if s.contains(optchar) {
|
|
continue;
|
|
}
|
|
} else {
|
|
// str has no short option at all (but perhaps it is the
|
|
// prefix of a single long option).
|
|
// Only complete short options if there is no character after the dash.
|
|
|
|
if s != L!("-") {
|
|
continue;
|
|
}
|
|
}
|
|
// It's a match.
|
|
let desc = o.desc.localize();
|
|
// Append a short-style option
|
|
if !self
|
|
.completions
|
|
.add(Completion::with_desc(o.option.clone(), desc.to_owned()))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check if the long style option matches.
|
|
if o.typ != CompleteOptionType::SingleLong
|
|
&& o.typ != CompleteOptionType::DoubleLong
|
|
{
|
|
continue;
|
|
}
|
|
|
|
let whole_opt = L!("-").repeat(o.expected_dash_count()) + o.option.as_utfstr();
|
|
if whole_opt.len() < s.len() {
|
|
continue;
|
|
}
|
|
if !s.starts_with("-") {
|
|
continue;
|
|
}
|
|
let anchor_start = !self.flags.fuzzy_match;
|
|
let Some(r#match) = string_fuzzy_match_string(s, &whole_opt, anchor_start) else {
|
|
continue;
|
|
};
|
|
|
|
let mut offset = 0;
|
|
let mut flags = CompleteFlags::empty();
|
|
|
|
if r#match.requires_full_replacement() {
|
|
flags = CompleteFlags::REPLACES_TOKEN;
|
|
} else {
|
|
offset = s.len();
|
|
}
|
|
|
|
// does this switch have any known arguments
|
|
let has_arg = !o.comp.is_empty();
|
|
// does this switch _require_ an argument
|
|
let req_arg = o.result_mode.requires_param;
|
|
|
|
if o.typ == CompleteOptionType::DoubleLong && (has_arg && !req_arg) {
|
|
// Optional arguments to a switch can only be handled using the '=', so we add it as
|
|
// a completion. By default we avoid using '=' and instead rely on '--switch
|
|
// switch-arg', since it is more commonly supported by homebrew getopt-like
|
|
// functions.
|
|
let completion = sprintf!("%s=", whole_opt.slice_from(offset));
|
|
|
|
// Append a long-style option with a mandatory trailing equal sign
|
|
if !self.completions.add(Completion::new(
|
|
completion,
|
|
o.desc.localize().to_owned(),
|
|
StringFuzzyMatch::exact_match(),
|
|
flags | CompleteFlags::NO_SPACE,
|
|
)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Append a long-style option
|
|
if !self.completions.add(Completion::new(
|
|
whole_opt.slice_from(offset).to_owned(),
|
|
o.desc.localize().to_owned(),
|
|
StringFuzzyMatch::exact_match(),
|
|
flags,
|
|
)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if has_force {
|
|
*out_do_file = true;
|
|
} else if !use_files {
|
|
*out_do_file = false;
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
/// Perform generic (not command-specific) expansions on the specified string.
|
|
fn complete_param_expand(
|
|
&mut self,
|
|
s: &wstr,
|
|
do_file: bool,
|
|
handle_as_special_cd: bool,
|
|
is_unterminated_brace: bool,
|
|
) {
|
|
if self.ctx.check_cancel() {
|
|
return;
|
|
}
|
|
let mut flags = self.expand_flags()
|
|
| ExpandFlags::FAIL_ON_CMDSUBST
|
|
| ExpandFlags::FOR_COMPLETIONS
|
|
| ExpandFlags::PRESERVE_HOME_TILDES;
|
|
if !do_file {
|
|
flags |= ExpandFlags::SKIP_WILDCARDS;
|
|
}
|
|
if is_unterminated_brace {
|
|
flags |= ExpandFlags::NO_SPACE_FOR_UNCLOSED_BRACE;
|
|
}
|
|
|
|
if handle_as_special_cd && do_file {
|
|
if self.flags.autosuggestion {
|
|
flags |= ExpandFlags::SPECIAL_FOR_CD_AUTOSUGGESTION;
|
|
}
|
|
flags |= ExpandFlags::DIRECTORIES_ONLY;
|
|
flags |= ExpandFlags::SPECIAL_FOR_CD;
|
|
}
|
|
|
|
// Squelch file descriptions per issue #254.
|
|
if self.flags.autosuggestion || do_file {
|
|
flags -= ExpandFlags::GEN_DESCRIPTIONS;
|
|
}
|
|
|
|
// Expand words separated by '=' separately, unless '=' is escaped or quoted.
|
|
// We have the following cases:
|
|
//
|
|
// --foo=bar => expand just bar
|
|
// -foo=bar => expand just bar
|
|
// foo=bar => expand the whole thing, and also just bar
|
|
//
|
|
// We also support colon separator (#2178). If there's more than one, prefer the last one.
|
|
let sep_index = if get_quote(s, s.len()).is_some() {
|
|
None
|
|
} else {
|
|
let mut end = s.len();
|
|
loop {
|
|
match s[..end].chars().rposition(|c| c == '=' || c == ':') {
|
|
Some(pos) => {
|
|
if !is_backslashed(s, pos) {
|
|
break Some(pos);
|
|
}
|
|
end = pos;
|
|
}
|
|
None => break None,
|
|
}
|
|
}
|
|
};
|
|
let complete_from_start = sep_index.is_none() || !string_prefixes_string(L!("-"), s);
|
|
|
|
let first_from_start = self.completions.len();
|
|
if complete_from_start {
|
|
let mut flags = flags;
|
|
// Don't do fuzzy matching for files if the string begins with a dash (issue #568). We could
|
|
// consider relaxing this if there was a preceding double-dash argument.
|
|
if string_prefixes_string(L!("-"), s) {
|
|
flags -= ExpandFlags::FUZZY_MATCH;
|
|
}
|
|
|
|
if matches!(
|
|
expand_to_receiver(s.to_owned(), &mut self.completions, flags, self.ctx, None)
|
|
.result,
|
|
ExpandResultCode::error | ExpandResultCode::overflow,
|
|
) {
|
|
flogf!(complete, "Error while expanding string '%s'", s);
|
|
}
|
|
Self::escape_opening_brackets(&mut self.completions[first_from_start..], s);
|
|
}
|
|
|
|
let Some(sep_index) = sep_index else {
|
|
return;
|
|
};
|
|
|
|
let sep_string = s.slice_from(sep_index + 1);
|
|
let mut local_completions = Vec::new();
|
|
if matches!(
|
|
expand_string(
|
|
sep_string.to_owned(),
|
|
&mut local_completions,
|
|
flags,
|
|
self.ctx,
|
|
None,
|
|
)
|
|
.result,
|
|
ExpandResultCode::error | ExpandResultCode::overflow
|
|
) {
|
|
flogf!(complete, "Error while expanding string '%s'", sep_string);
|
|
}
|
|
|
|
Self::escape_opening_brackets(&mut local_completions, s);
|
|
// Any COMPLETE_REPLACES_TOKEN will also stomp the separator. We need to "repair" them by
|
|
// inserting our separator and prefix.
|
|
let prefix_with_sep = s.as_char_slice()[..=sep_index].into();
|
|
for comp in &mut local_completions {
|
|
comp.prepend_token_prefix(prefix_with_sep);
|
|
comp.r#match.from_separator = true;
|
|
}
|
|
let _ = self.completions.extend(local_completions);
|
|
}
|
|
|
|
/// Complete the specified string as an environment variable.
|
|
/// Returns `true` if this was a variable, so we should stop completion.
|
|
fn complete_variable(&mut self, s: &wstr, start_offset: usize) -> bool {
|
|
let whole_var = s;
|
|
let var = whole_var.slice_from(start_offset);
|
|
let varlen = s.len() - start_offset;
|
|
let mut res = false;
|
|
|
|
for env_name in self.ctx.vars().get_names(EnvMode::empty()) {
|
|
let anchor_start = !self.flags.fuzzy_match;
|
|
let Some(r#match) = string_fuzzy_match_string(var, &env_name, anchor_start) else {
|
|
continue;
|
|
};
|
|
|
|
let mut flags = CompleteFlags::VARIABLE_NAME;
|
|
let comp = if !r#match.requires_full_replacement() {
|
|
// Take only the suffix.
|
|
env_name.slice_from(varlen).to_owned()
|
|
} else {
|
|
flags |= CompleteFlags::REPLACES_TOKEN | CompleteFlags::DONT_ESCAPE;
|
|
whole_var.slice_to(start_offset).to_owned() + env_name.as_utfstr()
|
|
};
|
|
|
|
let mut desc = WString::new();
|
|
if self.flags.descriptions && !self.flags.autosuggestion {
|
|
// $history can be huge, don't put all of it in the completion description; see
|
|
// #6288.
|
|
if env_name == "history" {
|
|
let history = History::with_name(&history_session_id(self.ctx.vars()));
|
|
for i in 1..std::cmp::min(history.size(), 64) {
|
|
if i > 1 {
|
|
desc.push(' ');
|
|
}
|
|
desc.push_utfstr(&expand_escape_string(
|
|
history.item_at_index(i).unwrap().str(),
|
|
));
|
|
}
|
|
} else {
|
|
// Can't use ctx.vars() here, it could be any variable.
|
|
let Some(var) = self.ctx.vars().get(&env_name) else {
|
|
continue;
|
|
};
|
|
|
|
let value = expand_escape_variable(&var);
|
|
desc = wgettext_fmt!(COMPLETE_VAR_DESC_VAL, value);
|
|
}
|
|
}
|
|
|
|
// Append matching environment variables
|
|
// TODO: need to propagate overflow here.
|
|
let _ = self
|
|
.completions
|
|
.add(Completion::new(comp, desc, r#match, flags));
|
|
|
|
res = true;
|
|
}
|
|
|
|
res
|
|
}
|
|
|
|
fn try_complete_variable(&mut self, s: &wstr) -> bool {
|
|
#[derive(PartialEq, Eq)]
|
|
enum Mode {
|
|
Unquoted,
|
|
SingleQuoted,
|
|
DoubleQuoted,
|
|
}
|
|
use Mode::*;
|
|
|
|
let mut mode = Unquoted;
|
|
|
|
// Get the position of the dollar heading a (possibly empty) run of valid variable characters.
|
|
let mut variable_start = None;
|
|
|
|
let mut skip_next = false;
|
|
for (in_pos, c) in s.chars().enumerate() {
|
|
if skip_next {
|
|
skip_next = false;
|
|
continue;
|
|
}
|
|
|
|
if !valid_var_name_char(c) {
|
|
// This character cannot be in a variable, reset the dollar.
|
|
variable_start = None;
|
|
}
|
|
|
|
match c {
|
|
'\\' => skip_next = true,
|
|
'$' => {
|
|
if mode == Unquoted || mode == DoubleQuoted {
|
|
variable_start = Some(in_pos);
|
|
}
|
|
}
|
|
'\'' => {
|
|
if mode == SingleQuoted {
|
|
mode = Unquoted;
|
|
} else if mode == Unquoted {
|
|
mode = SingleQuoted;
|
|
}
|
|
}
|
|
'"' => {
|
|
if mode == DoubleQuoted {
|
|
mode = Unquoted;
|
|
} else if mode == Unquoted {
|
|
mode = DoubleQuoted;
|
|
}
|
|
}
|
|
_ => {
|
|
// all other chars ignored here
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now complete if we have a variable start. Note the variable text may be empty; in that case
|
|
// don't generate an autosuggestion, but do allow tab completion.
|
|
let allow_empty = !self.flags.autosuggestion;
|
|
let text_is_empty = variable_start == Some(s.len() - 1);
|
|
if let Some(variable_start) = variable_start {
|
|
if allow_empty || !text_is_empty {
|
|
return self.complete_variable(s, variable_start + 1);
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Try to complete the specified string as a username. This is used by `~USER` type expansion.
|
|
///
|
|
/// Returns `false` if unable to complete, `true` otherwise
|
|
fn try_complete_user(&mut self, s: &wstr) -> bool {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
// The getpwent() function does not exist on Android. A Linux user on Android isn't
|
|
// really a user - each installed app gets an UID assigned. Listing all UID:s is not
|
|
// possible without root access, and doing a ~USER type expansion does not make sense
|
|
// since every app is sandboxed and can't access each other.
|
|
return false;
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
static SETPWENT_LOCK: Mutex<()> = Mutex::new(());
|
|
|
|
if s.char_at(0) != '~' || s.contains('/') {
|
|
return false;
|
|
}
|
|
|
|
let user_name = s.slice_from(1);
|
|
if user_name.contains('~') {
|
|
return false;
|
|
}
|
|
|
|
let start_time = Instant::now();
|
|
let mut result = false;
|
|
let name_len = s.len() - 1;
|
|
|
|
fn getpwent_name() -> Option<WString> {
|
|
let ptr = unsafe { libc::getpwent() };
|
|
if ptr.is_null() {
|
|
return None;
|
|
}
|
|
let pw = unsafe { ptr.read() };
|
|
Some(charptr2wcstring(pw.pw_name))
|
|
}
|
|
|
|
let _guard = SETPWENT_LOCK.lock().unwrap();
|
|
|
|
unsafe { libc::setpwent() };
|
|
while let Some(pw_name) = getpwent_name() {
|
|
if self.ctx.check_cancel() {
|
|
break;
|
|
}
|
|
|
|
if string_prefixes_string(user_name, &pw_name) {
|
|
let desc = wgettext_fmt!(COMPLETE_USER_DESC, &pw_name);
|
|
// Append a user name.
|
|
// TODO: propagate overflow?
|
|
let _ = self.completions.add(Completion::new(
|
|
pw_name.slice_from(name_len).to_owned(),
|
|
desc,
|
|
StringFuzzyMatch::exact_match(),
|
|
CompleteFlags::NO_SPACE,
|
|
));
|
|
result = true;
|
|
} else if string_prefixes_string_case_insensitive(user_name, &pw_name) {
|
|
let name = sprintf!("~%s", &pw_name);
|
|
let desc = wgettext_fmt!(COMPLETE_USER_DESC, &pw_name);
|
|
|
|
// Append a user name
|
|
// TODO: propagate overflow?
|
|
let _ = self.completions.add(Completion::new(
|
|
name,
|
|
desc,
|
|
StringFuzzyMatch::exact_match(),
|
|
CompleteFlags::REPLACES_TOKEN
|
|
| CompleteFlags::DONT_ESCAPE
|
|
| CompleteFlags::NO_SPACE,
|
|
));
|
|
result = true;
|
|
}
|
|
|
|
// If we've spent too much time (more than 200 ms) doing this give up.
|
|
if start_time.elapsed() > Duration::from_millis(200) {
|
|
break;
|
|
}
|
|
}
|
|
unsafe { libc::endpwent() };
|
|
|
|
result
|
|
}
|
|
}
|
|
|
|
/// If we have variable assignments, attempt to apply them in our parser. As soon as the return
|
|
/// value goes out of scope, the variables will be removed from the parser.
|
|
fn apply_var_assignments<T: AsRef<wstr>>(
|
|
&mut self,
|
|
var_assignments: &[T],
|
|
) -> Option<ScopeGuard<(), impl FnOnce(()) + 'ctx + use<'ctx, T>>> {
|
|
if !self.ctx.has_parser() || var_assignments.is_empty() {
|
|
return None;
|
|
}
|
|
let parser = self.ctx.parser();
|
|
|
|
let vars = parser.vars();
|
|
assert_eq!(
|
|
std::ptr::from_ref(self.ctx.vars()).cast::<()>(),
|
|
std::ptr::from_ref(vars).cast::<()>(),
|
|
"Don't know how to tab complete with a parser but a different variable set"
|
|
);
|
|
|
|
// clone of parse_execution_context_t::apply_variable_assignments.
|
|
// Crucially do NOT expand subcommands:
|
|
// VAR=(launch_missiles) cmd<tab>
|
|
// should not launch missiles.
|
|
// Note we also do NOT send --on-variable events.
|
|
let expand_flags = ExpandFlags::FAIL_ON_CMDSUBST;
|
|
let block = parser.push_block(Block::variable_assignment_block());
|
|
for var_assign in var_assignments {
|
|
let var_assign: &wstr = var_assign.as_ref();
|
|
let equals_pos = variable_assignment_equals_pos(var_assign)
|
|
.expect("All variable assignments should have equals position");
|
|
let variable_name = var_assign.as_char_slice()[..equals_pos].into();
|
|
let expression = var_assign.slice_from(equals_pos + 1);
|
|
|
|
let mut expression_expanded = Vec::new();
|
|
let expand_ret = expand_string(
|
|
expression.to_owned(),
|
|
&mut expression_expanded,
|
|
expand_flags,
|
|
self.ctx,
|
|
None,
|
|
);
|
|
// If expansion succeeds, set the value; if it fails (e.g. it has a cmdsub) set an empty
|
|
// value anyways.
|
|
let vals = if expand_ret.result == ExpandResultCode::ok {
|
|
expression_expanded
|
|
.into_iter()
|
|
.map(|c| c.completion)
|
|
.collect()
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
parser.set_var(
|
|
variable_name,
|
|
ParserEnvSetMode::new(EnvMode::LOCAL | EnvMode::EXPORT),
|
|
vals,
|
|
);
|
|
if self.ctx.check_cancel() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let parser = self.ctx.parser();
|
|
Some(ScopeGuard::new((), move |_| parser.pop_block(block)))
|
|
}
|
|
|
|
/// Complete a command by invoking user-specified completions.
|
|
fn complete_custom(&mut self, cmd: &wstr, cmdline: &wstr, ad: &mut CustomArgData) {
|
|
if self.ctx.check_cancel() {
|
|
return;
|
|
}
|
|
|
|
let is_autosuggest = self.flags.autosuggestion;
|
|
// Perhaps set a transient commandline so that custom completions
|
|
// builtin_commandline will refer to the wrapped command. But not if
|
|
// we're doing autosuggestions.
|
|
let _remove_transient = (!is_autosuggest).then(|| {
|
|
let parser = self.ctx.parser();
|
|
let saved_transient = parser
|
|
.libdata_mut()
|
|
.transient_commandline
|
|
.replace(cmdline.to_owned());
|
|
ScopeGuard::new((), move |_| {
|
|
parser.libdata_mut().transient_commandline = saved_transient;
|
|
})
|
|
});
|
|
|
|
// Maybe apply variable assignments.
|
|
let _restore_vars = self.apply_var_assignments(ad.var_assignments);
|
|
if self.ctx.check_cancel() {
|
|
return;
|
|
}
|
|
|
|
// Invoke any custom completions for this command.
|
|
self.complete_param_for_command(
|
|
cmd,
|
|
&ad.previous_argument,
|
|
&ad.current_argument,
|
|
!ad.had_ddash,
|
|
&mut ad.do_file,
|
|
);
|
|
}
|
|
|
|
// Invoke command-specific completions given by `arg_data`.
|
|
// Then, for each target wrapped by the given command, update the command
|
|
// line with that target and invoke this recursively.
|
|
// The command whose completions to use is given by `cmd`. The full command line is given by \p
|
|
// cmdline and the command's range in it is given by `cmdrange`. Note: the command range
|
|
// may have a different length than the command itself, because the command is unescaped (i.e.
|
|
// quotes removed).
|
|
fn walk_wrap_chain(
|
|
&mut self,
|
|
cmd: &wstr,
|
|
cmdline: &wstr,
|
|
cmdrange: SourceRange,
|
|
ad: &mut CustomArgData,
|
|
) {
|
|
// Limit our recursion depth. This prevents cycles in the wrap chain graph from overflowing.
|
|
if ad.wrap_depth > 24 {
|
|
return;
|
|
}
|
|
if self.ctx.check_cancel() {
|
|
return;
|
|
}
|
|
|
|
// Extract command from the command line and invoke the receiver with it.
|
|
self.complete_custom(cmd, cmdline, ad);
|
|
|
|
let targets = complete_get_wrap_targets(cmd);
|
|
let wrap_depth = ad.wrap_depth;
|
|
let mut ad = ScopeGuard::new(ad, |ad| ad.wrap_depth = wrap_depth);
|
|
ad.wrap_depth += 1;
|
|
|
|
for wt in targets {
|
|
// We may append to the variable assignment list; ensure we restore it.
|
|
let saved_var_count = ad.var_assignments.len();
|
|
let mut ad = ScopeGuard::new(&mut ad, |ad| {
|
|
assert!(
|
|
ad.var_assignments.len() >= saved_var_count,
|
|
"Should not delete var assignments"
|
|
);
|
|
ad.var_assignments.truncate(saved_var_count);
|
|
});
|
|
|
|
// Separate the wrap target into any variable assignments VAR=... and the command itself.
|
|
let mut wrapped_command = None;
|
|
let mut wrapped_command_offset_in_wt = None;
|
|
let tokenizer = Tokenizer::new(&wt, TokFlags(0));
|
|
for tok in tokenizer {
|
|
let mut tok_src = tok.get_source(&wt).to_owned();
|
|
if variable_assignment_equals_pos(&tok_src).is_some() {
|
|
ad.var_assignments.push(tok_src);
|
|
} else {
|
|
expand_command_token(self.ctx, &mut tok_src);
|
|
|
|
wrapped_command_offset_in_wt = Some(tok.offset());
|
|
wrapped_command = Some(tok_src);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Skip this wrapped command if empty, or if we've seen it before.
|
|
let Some((wrapped_command, wrapped_command_offset_in_wt)) =
|
|
Option::zip(wrapped_command, wrapped_command_offset_in_wt)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
if !ad.visited_wrapped_commands.insert(wrapped_command.clone()) {
|
|
continue;
|
|
}
|
|
|
|
// Construct a fake command line containing the wrap target.
|
|
// https://github.com/starkat99/widestring-rs/issues/37
|
|
let mut faux_commandline = cmdline.as_char_slice().to_vec();
|
|
faux_commandline.splice(std::ops::Range::from(cmdrange), wt.chars());
|
|
let faux_commandline = WString::from(faux_commandline);
|
|
|
|
// Recurse with our new command and command line.
|
|
let faux_source_range = SourceRange::new(
|
|
cmdrange.start() + wrapped_command_offset_in_wt,
|
|
wrapped_command.len(),
|
|
);
|
|
self.walk_wrap_chain(
|
|
&wrapped_command,
|
|
&faux_commandline,
|
|
faux_source_range,
|
|
***ad,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// If the argument contains a '[' typed by the user, completion by appending to the argument might
|
|
/// produce an invalid token (#5831).
|
|
///
|
|
/// Check if there is any unescaped, unquoted '['; if yes, make the completions replace the entire
|
|
/// argument instead of appending, so '[' will be escaped.
|
|
fn escape_opening_brackets(completions: &mut [Completion], argument: &wstr) {
|
|
let mut have_unquoted_unescaped_bracket = false;
|
|
let mut quote = None;
|
|
let mut escaped = false;
|
|
for c in argument.chars() {
|
|
have_unquoted_unescaped_bracket |= c == '[' && quote.is_none() && !escaped;
|
|
if escaped {
|
|
escaped = false;
|
|
} else if c == '\\' {
|
|
escaped = true;
|
|
} else if c == '\'' || c == '"' {
|
|
if quote == Some(c) {
|
|
// Closing a quote.
|
|
quote = None;
|
|
} else if quote.is_none() {
|
|
// Opening a quote.
|
|
quote = Some(c);
|
|
}
|
|
}
|
|
}
|
|
if !have_unquoted_unescaped_bracket {
|
|
return;
|
|
}
|
|
|
|
// Since completion_apply_to_command_line will escape the completion, we need to provide an
|
|
// unescaped version.
|
|
let Some(unescaped_argument) = unescape_string(
|
|
argument,
|
|
UnescapeStringStyle::Script(UnescapeFlags::INCOMPLETE),
|
|
) else {
|
|
return;
|
|
};
|
|
for comp in completions {
|
|
if comp.replaces_token() {
|
|
continue;
|
|
}
|
|
comp.flags |= CompleteFlags::REPLACES_TOKEN;
|
|
comp.flags |= CompleteFlags::DONT_ESCAPE_TILDES; // See #9073.
|
|
|
|
// We are grafting a completion that is expected to be escaped later. This will break
|
|
// if the original completion doesn't want escaping. Happily, this is only the case
|
|
// for username completion and variable name completion. They shouldn't end up here
|
|
// anyway because they won't contain '['.
|
|
if comp.flags.contains(CompleteFlags::DONT_ESCAPE) {
|
|
flog!(warning, "unexpected completion flag");
|
|
}
|
|
comp.completion.insert_utfstr(0, &unescaped_argument);
|
|
}
|
|
}
|
|
|
|
/// Set the `DUPLICATES_ARG` flag in any completion that duplicates an argument.
|
|
fn mark_completions_duplicating_arguments(
|
|
&mut self,
|
|
cmd: &wstr,
|
|
prefix: &wstr,
|
|
args: impl IntoIterator<Item = Tok>,
|
|
) {
|
|
// Get all the arguments, unescaped, into an array that we're going to bsearch.
|
|
let mut arg_strs: Vec<_> = args
|
|
.into_iter()
|
|
.map(|arg| arg.get_source(cmd))
|
|
.filter_map(|argstr| unescape_string(argstr, UnescapeStringStyle::default()))
|
|
.collect();
|
|
arg_strs.sort();
|
|
|
|
let mut comp_str;
|
|
for comp in self.completions.get_list_mut() {
|
|
comp_str = comp.completion.clone();
|
|
if !comp.replaces_token() {
|
|
comp_str.insert_utfstr(0, prefix);
|
|
}
|
|
if arg_strs.binary_search(&comp_str).is_ok() {
|
|
comp.flags |= CompleteFlags::DUPLICATES_ARGUMENT;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CmdString {
|
|
cmd: WString,
|
|
path: WString,
|
|
}
|
|
|
|
/// Find the full path and commandname from a command string `s`.
|
|
fn parse_cmd_string(s: &wstr, vars: &dyn Environment) -> CmdString {
|
|
let path_result = path_try_get_path(s, vars);
|
|
let found = path_result.err.is_none();
|
|
let mut path = path_result.path;
|
|
// Resolve commands that use relative paths because we compare full paths with "complete -p".
|
|
if found && !path.is_empty() && path.as_char_slice().first() != Some(&'/') {
|
|
if let Some(full_path) = wrealpath(&path) {
|
|
path = full_path;
|
|
}
|
|
}
|
|
|
|
// Make sure the path is not included in the command.
|
|
let cmd = if let Some(last_slash) = s.chars().rposition(|c| c == '/') {
|
|
&s[last_slash + 1..]
|
|
} else {
|
|
s
|
|
}
|
|
.to_owned();
|
|
|
|
CmdString { cmd, path }
|
|
}
|
|
|
|
/// Returns a description for the specified function, or an empty string if none.
|
|
fn complete_function_desc(f: &wstr) -> WString {
|
|
if let Some(props) = function::get_props(f) {
|
|
props.description.localize().to_owned()
|
|
} else {
|
|
WString::new()
|
|
}
|
|
}
|
|
|
|
fn leading_dash_count(s: &wstr) -> usize {
|
|
s.chars().take_while(|&c| c == '-').count()
|
|
}
|
|
|
|
/// Match a parameter.
|
|
fn param_match(e: &CompleteEntryOpt, optstr: &wstr) -> bool {
|
|
if e.typ == CompleteOptionType::ArgsOnly {
|
|
false
|
|
} else {
|
|
let dashes = leading_dash_count(optstr);
|
|
dashes == e.expected_dash_count() && e.option == optstr[dashes..]
|
|
}
|
|
}
|
|
|
|
/// Test if a string is an option with an argument, like --color=auto or -I/usr/include.
|
|
fn param_match2(e: &CompleteEntryOpt, optstr: &wstr) -> Option<usize> {
|
|
// We may get a complete_entry_opt_t with no options if it's just arguments.
|
|
if e.option.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Verify leading dashes.
|
|
let mut cursor = leading_dash_count(optstr);
|
|
if cursor != e.expected_dash_count() {
|
|
return None;
|
|
}
|
|
|
|
// Verify options match.
|
|
if !optstr.slice_from(cursor).starts_with(&e.option) {
|
|
return None;
|
|
}
|
|
cursor += e.option.len();
|
|
|
|
// Short options are like -DNDEBUG. Long options are like --color=auto. So check for an equal
|
|
// sign for long options.
|
|
assert!(e.typ != CompleteOptionType::Short);
|
|
if optstr.char_at(cursor) != '=' {
|
|
return None;
|
|
}
|
|
cursor += 1;
|
|
Some(cursor)
|
|
}
|
|
|
|
/// Parses a token of short options plus one optional parameter like
|
|
/// '-xzPARAM', where x and z are short options.
|
|
///
|
|
/// Returns the position of the last option character (e.g. the position of z which is 2).
|
|
/// Everything after that is assumed to be part of the parameter.
|
|
/// Returns wcstring::npos if there is no valid short option.
|
|
fn short_option_pos(arg: &wstr, options: &[CompleteEntryOpt]) -> Option<usize> {
|
|
if arg.len() <= 1 || leading_dash_count(arg) != 1 {
|
|
return None;
|
|
}
|
|
for (pos, arg_char) in arg.chars().enumerate().skip(1) {
|
|
let r#match = options
|
|
.iter()
|
|
.find(|o| o.typ == CompleteOptionType::Short && o.option.char_at(0) == arg_char);
|
|
if let Some(r#match) = r#match {
|
|
if r#match.result_mode.requires_param {
|
|
return Some(pos);
|
|
}
|
|
} else {
|
|
// The first character after the dash is not a valid option.
|
|
if pos == 1 {
|
|
return None;
|
|
}
|
|
return Some(pos - 1);
|
|
}
|
|
}
|
|
|
|
Some(arg.len() - 1)
|
|
}
|
|
|
|
fn expand_command_token(ctx: &OperationContext<'_>, cmd_tok: &mut WString) -> bool {
|
|
// TODO: we give up if the first token expands to more than one argument. We could handle
|
|
// that case by propagating arguments.
|
|
// Also we could expand wildcards.
|
|
expand_one(
|
|
cmd_tok,
|
|
ExpandFlags::FAIL_ON_CMDSUBST | ExpandFlags::SKIP_WILDCARDS,
|
|
ctx,
|
|
None,
|
|
)
|
|
}
|
|
|
|
/// Add an unexpanded completion "rule" to generate completions from for a command.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// The command 'gcc -o' requires that a file follows it, so the `requires_param` mode is suitable.
|
|
/// This can be done using the following line:
|
|
///
|
|
/// complete -c gcc -s o -r
|
|
///
|
|
/// The command 'grep -d' required that one of the strings 'read', 'skip' or 'recurse' is used. As
|
|
/// such, it is suitable to specify that a completion requires one of them. This can be done using
|
|
/// the following line:
|
|
///
|
|
/// complete -c grep -s d -x -a "read skip recurse"
|
|
///
|
|
/// - `cmd`: Command to complete.
|
|
/// - `cmd_is_path`: If `true`, cmd will be interpreted as the absolute
|
|
/// path of the program (optionally containing wildcards), otherwise it
|
|
/// will be interpreted as the command name.
|
|
/// - `option`: The name of an option.
|
|
/// - `option_type`: The type of option: can be option_type_short (-x),
|
|
/// option_type_single_long (-foo), option_type_double_long (--bar).
|
|
/// - `result_mode`: Controls how to search further completions when this completion has been
|
|
/// successfully matched.
|
|
/// - `comp`: A space separated list of completions which may contain subshells.
|
|
/// - `desc`: A description of the completion.
|
|
/// - `condition`: a command to be run to check it this completion should be used. If `condition`
|
|
/// is empty, the completion is always used.
|
|
/// - `flags`: A set of completion flags
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn complete_add(
|
|
cmd: WString,
|
|
cmd_is_path: bool,
|
|
option: WString,
|
|
option_type: CompleteOptionType,
|
|
result_mode: CompletionMode,
|
|
condition: Vec<WString>,
|
|
comp: WString,
|
|
desc: WString,
|
|
flags: CompleteFlags,
|
|
) {
|
|
// option should be empty iff the option type is arguments only.
|
|
assert_eq!(
|
|
option.is_empty(),
|
|
(option_type == CompleteOptionType::ArgsOnly)
|
|
);
|
|
|
|
// Lock the lock that allows us to edit the completion entry list.
|
|
let mut completion_map = COMPLETION_MAP.lock().expect("mutex poisoned");
|
|
let c = completion_map
|
|
.entry(CompletionEntryIndex {
|
|
name: cmd,
|
|
is_path: cmd_is_path,
|
|
})
|
|
.or_insert_with(CompletionEntry::new);
|
|
|
|
// Create our new option.
|
|
let opt = CompleteEntryOpt {
|
|
option,
|
|
typ: option_type,
|
|
result_mode,
|
|
comp,
|
|
// The external source is a completion script in `share`,
|
|
// from which `build_tools/fish_xgettext.fish` extracts descriptions.
|
|
desc: LocalizableString::from_external_source(desc),
|
|
conditions: condition,
|
|
flags,
|
|
};
|
|
c.add_option(opt);
|
|
}
|
|
|
|
/// Remove a previously defined completion.
|
|
pub fn complete_remove(cmd: WString, cmd_is_path: bool, option: &wstr, typ: CompleteOptionType) {
|
|
let mut completion_map = COMPLETION_MAP.lock().expect("mutex poisoned");
|
|
let idx = CompletionEntryIndex {
|
|
name: cmd,
|
|
is_path: cmd_is_path,
|
|
};
|
|
if let Some(c) = completion_map.get_mut(&idx) {
|
|
let delete_it = c.remove_option(option, typ);
|
|
if delete_it {
|
|
completion_map.remove(&idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Removes all completions for a given command.
|
|
pub fn complete_remove_all(cmd: WString, cmd_is_path: bool, explicit: bool) {
|
|
let mut completion_map = COMPLETION_MAP.lock().expect("mutex poisoned");
|
|
let idx = CompletionEntryIndex {
|
|
name: cmd,
|
|
is_path: cmd_is_path,
|
|
};
|
|
let removed = completion_map.remove(&idx).is_some();
|
|
WRAPPER_MAP.lock().unwrap().remove(&idx.name);
|
|
if explicit && !removed && !idx.is_path {
|
|
COMPLETION_TOMBSTONES.lock().unwrap().insert(idx.name);
|
|
}
|
|
}
|
|
|
|
/// Returns all completions of the command cmd.
|
|
/// If `ctx` contains a parser, this will autoload functions and completions as needed.
|
|
/// If it does not contain a parser, then any completions which need autoloading will be returned.
|
|
pub fn complete(
|
|
cmd_with_subcmds: &wstr,
|
|
flags: CompletionRequestOptions,
|
|
ctx: &OperationContext,
|
|
) -> (Vec<Completion>, Vec<WString>) {
|
|
// Determine the innermost subcommand.
|
|
let cmdsubst = get_cmdsubst_extent(cmd_with_subcmds, cmd_with_subcmds.len());
|
|
let cmd = cmd_with_subcmds[cmdsubst].to_owned();
|
|
let mut completer = Completer::new(ctx, flags);
|
|
completer.perform_for_commandline(cmd);
|
|
|
|
(
|
|
completer.acquire_completions(),
|
|
completer.acquire_needs_load(),
|
|
)
|
|
}
|
|
|
|
/// Print the short switch `opt`, and the argument `arg` to the specified
|
|
/// [`WString`], but only if `argument` isn't an empty string.
|
|
fn append_switch_short_arg(out: &mut WString, opt: char, arg: &wstr) {
|
|
if arg.is_empty() {
|
|
return;
|
|
}
|
|
|
|
sprintf!(=> out, " -%c %s", opt, escape(arg));
|
|
}
|
|
fn append_switch_long_arg(out: &mut WString, opt: &wstr, arg: &wstr) {
|
|
if arg.is_empty() {
|
|
return;
|
|
}
|
|
|
|
sprintf!(=> out, " --%s %s", opt, escape(arg));
|
|
}
|
|
fn append_switch_short(out: &mut WString, opt: char) {
|
|
sprintf!(=> out, " -%c", opt);
|
|
}
|
|
fn append_switch_long(out: &mut WString, opt: &wstr) {
|
|
sprintf!(=> out, " --%s", opt);
|
|
}
|
|
|
|
fn completion2string(index: &CompletionEntryIndex, o: &CompleteEntryOpt) -> WString {
|
|
let mut out = WString::from(L!("complete"));
|
|
|
|
if o.flags.contains(CompleteFlags::DONT_SORT) {
|
|
append_switch_short(&mut out, 'k');
|
|
}
|
|
|
|
if o.result_mode.no_files && o.result_mode.requires_param {
|
|
append_switch_long(&mut out, L!("exclusive"));
|
|
} else if o.result_mode.no_files {
|
|
append_switch_long(&mut out, L!("no-files"));
|
|
} else if o.result_mode.force_files {
|
|
append_switch_long(&mut out, L!("force-files"));
|
|
} else if o.result_mode.requires_param {
|
|
append_switch_long(&mut out, L!("require-parameter"));
|
|
}
|
|
|
|
if index.is_path {
|
|
append_switch_short_arg(&mut out, 'p', &index.name);
|
|
} else {
|
|
out.push(' ');
|
|
out.push_utfstr(&escape(&index.name));
|
|
}
|
|
|
|
match o.typ {
|
|
CompleteOptionType::ArgsOnly => {}
|
|
CompleteOptionType::Short => append_switch_short_arg(&mut out, 's', &o.option[..1]),
|
|
CompleteOptionType::SingleLong => append_switch_short_arg(&mut out, 'o', &o.option),
|
|
CompleteOptionType::DoubleLong => append_switch_short_arg(&mut out, 'l', &o.option),
|
|
}
|
|
|
|
append_switch_short_arg(&mut out, 'd', o.desc.localize());
|
|
append_switch_short_arg(&mut out, 'a', &o.comp);
|
|
for c in &o.conditions {
|
|
append_switch_short_arg(&mut out, 'n', c);
|
|
}
|
|
out.push('\n');
|
|
|
|
out
|
|
}
|
|
|
|
/// If the cmd contains a partial executable extension, return the stripped
|
|
/// command and missing part of the full extension.
|
|
/// E.g. `cmd.e` -> `Some(("cmd", "xe"))``
|
|
fn strip_partial_executable_suffix(cmd: &wstr) -> Option<(&wstr, &wstr)> {
|
|
if !cfg!(cygwin) {
|
|
return None;
|
|
}
|
|
|
|
[
|
|
// (<cmd suffix>, <completion for full ".exe">)
|
|
(L!(".exe"), L!("")),
|
|
(L!(".ex"), L!("e")),
|
|
(L!(".e"), L!("xe")),
|
|
(L!("."), L!("exe")),
|
|
]
|
|
.into_iter()
|
|
.find(|(ext, _)| string_suffixes_string_case_insensitive(ext, cmd))
|
|
.map(|(ext, comp)| (&cmd[0..cmd.len() - ext.len()], comp))
|
|
}
|
|
|
|
/// Load command-specific completions for the specified command.
|
|
/// Returns `true` if something new was loaded, `false` if not.
|
|
pub fn complete_load(cmd: &wstr, parser: &Parser) -> bool {
|
|
if COMPLETION_TOMBSTONES.lock().unwrap().contains(cmd) {
|
|
return false;
|
|
}
|
|
|
|
let mut loaded_new = false;
|
|
|
|
// We have to load this as a function, since it may define a --wraps or signature.
|
|
// See issue #2466.
|
|
if function::load(cmd, parser) {
|
|
// We autoloaded something; check if we have a --wraps.
|
|
loaded_new |= complete_wrap_map().get(cmd).is_some();
|
|
}
|
|
|
|
// It's important to NOT hold the lock around completion loading.
|
|
// We need to take the lock to decide what to load, drop it to perform the load, then reacquire
|
|
// it.
|
|
// Note we only look at the global fish_function_path and fish_complete_path.
|
|
let path_to_load = COMPLETION_AUTOLOADER
|
|
.lock()
|
|
.expect("mutex poisoned")
|
|
.resolve_command(cmd, EnvStack::globals());
|
|
match path_to_load {
|
|
AutoloadResult::Path(path_to_load) => {
|
|
Autoload::perform_autoload(&path_to_load, parser);
|
|
COMPLETION_AUTOLOADER
|
|
.lock()
|
|
.expect("mutex poisoned")
|
|
.mark_autoload_finished(cmd);
|
|
loaded_new = true;
|
|
}
|
|
AutoloadResult::None => {
|
|
// On Cygwin, if we failed to find a completion for "foo.exe", try "foo"
|
|
if let Some(stripped) = strip_executable_suffix(cmd) {
|
|
loaded_new = complete_load(stripped, parser);
|
|
}
|
|
}
|
|
AutoloadResult::Loaded | AutoloadResult::Pending => {}
|
|
}
|
|
loaded_new
|
|
}
|
|
|
|
/// Return a list of all current completions.
|
|
/// Used by the bare `complete`, loaded completions are printed out as commands
|
|
pub fn complete_print(cmd: &wstr) -> WString {
|
|
let mut out = WString::new();
|
|
|
|
// Get references to our completions and sort them by order.
|
|
let completions = COMPLETION_MAP.lock().expect("poisoned mutex");
|
|
let mut completion_refs: Vec<_> = completions.iter().collect();
|
|
completion_refs.sort_by_key(|(_, c)| c.order);
|
|
|
|
for (key, entry) in completion_refs {
|
|
if !cmd.is_empty() && key.name != cmd {
|
|
continue;
|
|
}
|
|
|
|
// Output in reverse order to preserve legacy behavior (see #9221).
|
|
for o in entry.get_options().iter().rev() {
|
|
out.push_utfstr(&completion2string(key, o));
|
|
}
|
|
}
|
|
|
|
// Append wraps.
|
|
let wrappers = WRAPPER_MAP.lock().expect("poisoned mutex");
|
|
for (src, targets) in wrappers.iter() {
|
|
if !cmd.is_empty() && src != cmd {
|
|
continue;
|
|
}
|
|
for target in targets {
|
|
out.push_utfstr(L!("complete "));
|
|
out.push_utfstr(&escape(src));
|
|
append_switch_long_arg(&mut out, L!("wraps"), target);
|
|
out.push_utfstr(L!("\n"));
|
|
}
|
|
}
|
|
|
|
out
|
|
}
|
|
|
|
/// Observes that fish_complete_path has changed.
|
|
pub fn complete_invalidate_path() {
|
|
// TODO: here we unload all completions for commands that are loaded by the autoloader. We also
|
|
// unload any completions that the user may specified on the command line. We should in
|
|
// principle track those completions loaded by the autoloader alone.
|
|
|
|
let cmds = COMPLETION_AUTOLOADER
|
|
.lock()
|
|
.expect("mutex poisoned")
|
|
.get_autoloaded_commands();
|
|
for cmd in cmds {
|
|
complete_remove_all(cmd, /*cmd_is_path=*/ false, /*explicit=*/ false);
|
|
}
|
|
}
|
|
|
|
/// Adds a "wrap target." A wrap target is a command that completes like another command.
|
|
pub fn complete_add_wrapper(command: WString, new_target: WString) -> bool {
|
|
if command.is_empty() || new_target.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
// If the command and the target are the same,
|
|
// there's no point in following the wrap-chain because we'd only complete the same thing.
|
|
// TODO: This should maybe include full cycle detection.
|
|
if command == new_target {
|
|
return false;
|
|
}
|
|
|
|
let mut wrappers = WRAPPER_MAP.lock().expect("poisoned mutex");
|
|
let targets = wrappers.entry(command).or_default();
|
|
// If it's already present, we do nothing.
|
|
if !targets.contains(&new_target) {
|
|
targets.push(new_target);
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
/// Removes a wrap target.
|
|
pub fn complete_remove_wrapper(command: WString, target_to_remove: &wstr) -> bool {
|
|
if command.is_empty() || target_to_remove.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
let mut wrappers = WRAPPER_MAP.lock().expect("poisoned mutex");
|
|
let mut result = false;
|
|
for targets in wrappers.values_mut() {
|
|
if let Some(pos) = targets.iter().position(|t| t == target_to_remove) {
|
|
targets.remove(pos);
|
|
result = true;
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Returns a list of wrap targets for a given command.
|
|
pub fn complete_wrap_map() -> MutexGuard<'static, HashMap<WString, Vec<WString>>> {
|
|
WRAPPER_MAP.lock().unwrap()
|
|
}
|
|
|
|
/// Returns a list of wrap targets for a given command.
|
|
pub fn complete_get_wrap_targets(command: &wstr) -> Vec<WString> {
|
|
if command.is_empty() {
|
|
return vec![];
|
|
}
|
|
|
|
let wrappers = WRAPPER_MAP.lock().expect("poisoned mutex");
|
|
wrappers.get(command).cloned().unwrap_or_default()
|
|
}
|
|
|
|
#[derive(Clone, Copy, Default)]
|
|
pub struct CompletionRequestOptions {
|
|
/// Requesting autosuggestion
|
|
pub autosuggestion: bool,
|
|
/// Make descriptions
|
|
pub descriptions: bool,
|
|
/// If set, we do not require a prefix match
|
|
pub fuzzy_match: bool,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
CompleteFlags, CompleteOptionType, CompletionMode, CompletionRequestOptions, complete,
|
|
complete_add, complete_add_wrapper, complete_get_wrap_targets, complete_remove_wrapper,
|
|
sort_and_prioritize,
|
|
};
|
|
use crate::abbrs::{self, Abbreviation, with_abbrs_mut};
|
|
use crate::common::str2wcstring;
|
|
use crate::env::{EnvMode, EnvSetMode, Environment as _};
|
|
use crate::io::IoChain;
|
|
use crate::operation_context::{
|
|
EXPANSION_LIMIT_BACKGROUND, EXPANSION_LIMIT_DEFAULT, OperationContext, no_cancel,
|
|
};
|
|
use crate::parser::ParserEnvSetMode;
|
|
use crate::prelude::*;
|
|
use crate::reader::completion_apply_to_command_line;
|
|
use crate::tests::prelude::*;
|
|
use fish_wcstringutil::join_strings;
|
|
use std::collections::HashMap;
|
|
use std::ffi::CString;
|
|
|
|
/// Joins a std::vector<wcstring> via commas.
|
|
fn comma_join(lst: Vec<WString>) -> WString {
|
|
join_strings(&lst, ',')
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn test_complete() {
|
|
let _cleanup = test_init();
|
|
let vars = PwdEnvironment {
|
|
parent: TestEnvironment {
|
|
vars: HashMap::from([
|
|
(L!("Foo1").to_owned(), WString::new()),
|
|
(L!("Foo2").to_owned(), WString::new()),
|
|
(L!("Foo3").to_owned(), WString::new()),
|
|
(L!("Bar1").to_owned(), WString::new()),
|
|
(L!("Bar2").to_owned(), WString::new()),
|
|
(L!("Bar3").to_owned(), WString::new()),
|
|
(L!("alpha").to_owned(), WString::new()),
|
|
(L!("ALPHA!").to_owned(), WString::new()),
|
|
(L!("gamma1").to_owned(), WString::new()),
|
|
(L!("GAMMA2").to_owned(), WString::new()),
|
|
(L!("SOMEDIR").to_owned(), L!("/").to_owned()),
|
|
(L!("SOMEVAR").to_owned(), WString::new()),
|
|
]),
|
|
},
|
|
};
|
|
|
|
let parser = TestParser::new();
|
|
let ctx = OperationContext::test_only_foreground(&parser, &vars, Box::new(no_cancel));
|
|
|
|
let do_complete =
|
|
|cmd: &wstr, flags: CompletionRequestOptions| complete(cmd, flags, &ctx).0;
|
|
|
|
let mut completions = do_complete(L!("$"), CompletionRequestOptions::default());
|
|
sort_and_prioritize(&mut completions, CompletionRequestOptions::default());
|
|
assert_eq!(
|
|
completions
|
|
.into_iter()
|
|
.map(|c| c.completion.to_string())
|
|
.collect::<Vec<_>>(),
|
|
[
|
|
"alpha", "ALPHA!", "Bar1", "Bar2", "Bar3", "Foo1", "Foo2", "Foo3", "gamma1",
|
|
"GAMMA2", "PWD", "SOMEDIR", "SOMEVAR",
|
|
]
|
|
.into_iter()
|
|
.map(|s| s.to_owned())
|
|
.collect::<Vec<_>>()
|
|
);
|
|
|
|
// Smartcase test. Lowercase inputs match both lowercase and uppercase.
|
|
let mut completions = do_complete(L!("$a"), CompletionRequestOptions::default());
|
|
sort_and_prioritize(&mut completions, CompletionRequestOptions::default());
|
|
|
|
assert_eq!(completions.len(), 2);
|
|
assert_eq!(completions[0].completion, L!("$ALPHA!"));
|
|
assert_eq!(completions[1].completion, L!("lpha"));
|
|
|
|
let mut completions = do_complete(L!("$F"), CompletionRequestOptions::default());
|
|
sort_and_prioritize(&mut completions, CompletionRequestOptions::default());
|
|
assert_eq!(completions.len(), 3);
|
|
assert_eq!(completions[0].completion, L!("oo1"));
|
|
assert_eq!(completions[1].completion, L!("oo2"));
|
|
assert_eq!(completions[2].completion, L!("oo3"));
|
|
|
|
completions = do_complete(L!("$1"), CompletionRequestOptions::default());
|
|
sort_and_prioritize(&mut completions, CompletionRequestOptions::default());
|
|
assert_eq!(completions, vec![]);
|
|
|
|
let fuzzy_options = CompletionRequestOptions {
|
|
fuzzy_match: true,
|
|
..Default::default()
|
|
};
|
|
let mut completions = do_complete(L!("$1"), fuzzy_options);
|
|
sort_and_prioritize(&mut completions, fuzzy_options);
|
|
assert_eq!(completions.len(), 3);
|
|
assert_eq!(completions[0].completion, L!("$Bar1"));
|
|
assert_eq!(completions[1].completion, L!("$Foo1"));
|
|
assert_eq!(completions[2].completion, L!("$gamma1"));
|
|
|
|
let _ = std::fs::remove_dir_all("test/complete_test");
|
|
std::fs::create_dir_all("test/complete_test").unwrap();
|
|
std::fs::write("test/complete_test/has space", []).unwrap();
|
|
std::fs::write("test/complete_test/bracket[abc]", []).unwrap();
|
|
#[cfg(not(cygwin))]
|
|
// Backslashes and colons are not legal filename characters on WIN32/CYGWIN
|
|
{
|
|
std::fs::write(r"test/complete_test/gnarlybracket\[abc]", []).unwrap();
|
|
std::fs::write(r"test/complete_test/colon:TTestWithColon", []).unwrap();
|
|
std::fs::create_dir_all("test/complete_test/cwd-for-colon").unwrap();
|
|
std::fs::write(r"test/complete_test/cwd-for-colon/test-file-in-cwd", []).unwrap();
|
|
}
|
|
std::fs::write(r"test/complete_test/equal=abc", []).unwrap();
|
|
// On MSYS, the executable bit cannot be set manually, is set automatically
|
|
// based on the file content/type. So make it a shell script
|
|
std::fs::write("test/complete_test/testfile", "#!/bin/sh").unwrap();
|
|
let testfile = CString::new("test/complete_test/testfile").unwrap();
|
|
assert_eq!(unsafe { libc::chmod(testfile.as_ptr(), 0o700,) }, 0);
|
|
std::fs::create_dir_all("test/complete_test/foo1").unwrap();
|
|
std::fs::create_dir_all("test/complete_test/foo2").unwrap();
|
|
std::fs::create_dir_all("test/complete_test/foo3").unwrap();
|
|
|
|
completions = do_complete(
|
|
L!("echo (test/complete_test/testfil"),
|
|
CompletionRequestOptions::default(),
|
|
);
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("e"));
|
|
|
|
completions = do_complete(
|
|
L!("echo (ls test/complete_test/testfil"),
|
|
CompletionRequestOptions::default(),
|
|
);
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("e"));
|
|
|
|
completions = do_complete(
|
|
L!("echo (command ls test/complete_test/testfil"),
|
|
CompletionRequestOptions::default(),
|
|
);
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("e"));
|
|
|
|
// Completing after spaces - see #2447
|
|
completions = do_complete(
|
|
L!("echo (ls test/complete_test/has\\ "),
|
|
CompletionRequestOptions::default(),
|
|
);
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("space"));
|
|
|
|
#[cfg(not(cygwin))]
|
|
// Backslashes and colons are not legal filename characters on WIN32/CYGWIN
|
|
{
|
|
macro_rules! whole_token_completion_dominates {
|
|
(
|
|
$cmd:literal,
|
|
$options:expr,
|
|
$completion_from_token_start:literal,
|
|
$completion_from_separator:literal,
|
|
) => {
|
|
completions = do_complete(L!($cmd), $options);
|
|
let actual: Vec<_> = completions
|
|
.iter()
|
|
.map(|c| (c.completion.as_utfstr(), c.r#match.from_separator))
|
|
.collect();
|
|
assert_eq!(
|
|
actual,
|
|
[
|
|
(
|
|
L!($completion_from_token_start),
|
|
/*from_separator=*/ false,
|
|
),
|
|
(
|
|
L!($completion_from_separator),
|
|
/*from_separator=*/ true,
|
|
),
|
|
]
|
|
);
|
|
let c0 = &completions[0];
|
|
let c1 = &completions[1];
|
|
// Might be replacing.
|
|
// Completion pager will only show better (lower) rank.
|
|
assert!(c0.r#match.rank() < c1.r#match.rank());
|
|
};
|
|
}
|
|
|
|
parser.pushd("test/complete_test/cwd-for-colon");
|
|
whole_token_completion_dominates!(
|
|
": ../colon:",
|
|
CompletionRequestOptions::default(),
|
|
"TTestWithColon",
|
|
"test-file-in-cwd",
|
|
);
|
|
// Even when it has a case mismatch.
|
|
whole_token_completion_dominates!(
|
|
": ../colon:t",
|
|
CompletionRequestOptions::default(),
|
|
"../colon:TTestWithColon",
|
|
"est-file-in-cwd",
|
|
);
|
|
// Even when it is not a prefix.
|
|
whole_token_completion_dominates!(
|
|
": ../colon:Tes",
|
|
fuzzy_options,
|
|
"../colon:TTestWithColon",
|
|
"../colon:test-file-in-cwd",
|
|
);
|
|
parser.popd();
|
|
}
|
|
|
|
macro_rules! unique_completion_applies_as {
|
|
( $cmdline:expr, $completion_result:expr, $applied:expr $(,)? ) => {
|
|
let cmdline = L!($cmdline);
|
|
let completions = do_complete(cmdline, CompletionRequestOptions::default());
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(
|
|
completions[0].completion,
|
|
L!($completion_result),
|
|
"completion mismatch"
|
|
);
|
|
let mut cursor = cmdline.len();
|
|
let newcmdline = completion_apply_to_command_line(
|
|
&ctx,
|
|
&completions[0].completion,
|
|
completions[0].flags,
|
|
cmdline,
|
|
&mut cursor,
|
|
/*append_only=*/ false,
|
|
/*is_unique=*/ true,
|
|
);
|
|
assert_eq!(newcmdline, L!($applied), "apply result mismatch");
|
|
};
|
|
}
|
|
|
|
unique_completion_applies_as!(
|
|
"touch test/complete_test/{testfi",
|
|
r"le",
|
|
"touch test/complete_test/{testfile",
|
|
);
|
|
|
|
// Brackets - see #5831
|
|
unique_completion_applies_as!(
|
|
"touch test/complete_test/bracket[",
|
|
"test/complete_test/bracket[abc]",
|
|
"touch 'test/complete_test/bracket[abc]' ",
|
|
);
|
|
unique_completion_applies_as!(
|
|
"echo (ls test/complete_test/bracket[",
|
|
"test/complete_test/bracket[abc]",
|
|
"echo (ls 'test/complete_test/bracket[abc]' ",
|
|
);
|
|
#[cfg(not(cygwin))]
|
|
// Backslashes are not legal filename characters on WIN32/CYGWIN
|
|
{
|
|
unique_completion_applies_as!(
|
|
r"touch test/complete_test/gnarlybracket\\[",
|
|
r"test/complete_test/gnarlybracket\[abc]",
|
|
r"touch 'test/complete_test/gnarlybracket\\[abc]' ",
|
|
);
|
|
unique_completion_applies_as!(
|
|
r"a=test/complete_test/bracket[",
|
|
r"test/complete_test/bracket[abc]",
|
|
r"a='test/complete_test/bracket[abc]' ",
|
|
);
|
|
}
|
|
|
|
#[cfg(not(cygwin))]
|
|
// Colons are not legal filename characters on WIN32/CYGWIN
|
|
{
|
|
parser.pushd("test/complete_test/cwd-for-colon");
|
|
unique_completion_applies_as!(
|
|
r"touch ../colon",
|
|
r":TTestWithColon",
|
|
r"touch ../colon:TTestWithColon ",
|
|
);
|
|
|
|
unique_completion_applies_as!(
|
|
r#"touch "../colon:"#,
|
|
r"TTestWithColon",
|
|
r#"touch "../colon:TTestWithColon" "#,
|
|
);
|
|
parser.popd();
|
|
}
|
|
|
|
unique_completion_applies_as!("echo $SOMEV", r"AR", "echo $SOMEVAR ");
|
|
unique_completion_applies_as!("echo $SOMED", r"IR", "echo $SOMEDIR/");
|
|
unique_completion_applies_as!(r#"echo "$SOMED"#, r"IR", r#"echo "$SOMEDIR/"#);
|
|
|
|
// #8820
|
|
let mut cursor_pos = 11;
|
|
let newcmdline = completion_apply_to_command_line(
|
|
&ctx,
|
|
L!("Debug/"),
|
|
CompleteFlags::REPLACES_TOKEN | CompleteFlags::NO_SPACE,
|
|
L!("mv debug debug"),
|
|
&mut cursor_pos,
|
|
true,
|
|
/*is_unique=*/ false,
|
|
);
|
|
assert_eq!(newcmdline, L!("mv debug Debug/"));
|
|
|
|
// Add a function and test completing it in various ways.
|
|
parser.eval(L!("function scuttlebutt; end"), &IoChain::new());
|
|
|
|
// Complete a function name.
|
|
completions = do_complete(L!("echo (scuttlebut"), CompletionRequestOptions::default());
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("t"));
|
|
|
|
// But not with the command prefix.
|
|
completions = do_complete(
|
|
L!("echo (command scuttlebut"),
|
|
CompletionRequestOptions::default(),
|
|
);
|
|
assert_eq!(&completions, &[]);
|
|
|
|
// Not with the builtin prefix.
|
|
let completions = do_complete(
|
|
L!("echo (builtin scuttlebut"),
|
|
CompletionRequestOptions::default(),
|
|
);
|
|
assert_eq!(&completions, &[]);
|
|
|
|
// Not after a redirection.
|
|
let completions = do_complete(
|
|
L!("echo hi > scuttlebut"),
|
|
CompletionRequestOptions::default(),
|
|
);
|
|
assert_eq!(&completions, &[]);
|
|
|
|
// Trailing spaces (#1261).
|
|
let no_files = CompletionMode {
|
|
no_files: true,
|
|
..Default::default()
|
|
};
|
|
complete_add(
|
|
L!("foobarbaz").into(),
|
|
false,
|
|
WString::new(),
|
|
CompleteOptionType::ArgsOnly,
|
|
no_files,
|
|
vec![],
|
|
L!("qux").into(),
|
|
WString::new(),
|
|
CompleteFlags::AUTO_SPACE,
|
|
);
|
|
let completions = do_complete(L!("foobarbaz "), CompletionRequestOptions::default());
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("qux"));
|
|
|
|
// Don't complete variable names in single quotes (#1023).
|
|
let completions = do_complete(L!("echo '$Foo"), CompletionRequestOptions::default());
|
|
assert_eq!(completions, vec![]);
|
|
let completions = do_complete(L!("echo \\$Foo"), CompletionRequestOptions::default());
|
|
assert_eq!(completions, vec![]);
|
|
|
|
// File completions.
|
|
let completions = do_complete(
|
|
L!("cat test/complete_test/te"),
|
|
CompletionRequestOptions::default(),
|
|
);
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("stfile"));
|
|
let completions = do_complete(
|
|
L!("echo sup > test/complete_test/te"),
|
|
CompletionRequestOptions::default(),
|
|
);
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("stfile"));
|
|
let completions = do_complete(
|
|
L!("echo sup > test/complete_test/te"),
|
|
CompletionRequestOptions::default(),
|
|
);
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("stfile"));
|
|
|
|
parser.pushd("test/complete_test");
|
|
let completions = do_complete(L!("cat te"), CompletionRequestOptions::default());
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("stfile"));
|
|
assert!(!completions[0].replaces_token());
|
|
assert!(
|
|
!(completions[0]
|
|
.flags
|
|
.contains(CompleteFlags::DUPLICATES_ARGUMENT))
|
|
);
|
|
let completions = do_complete(L!("cat testfile te"), CompletionRequestOptions::default());
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("stfile"));
|
|
assert!(
|
|
completions[0]
|
|
.flags
|
|
.contains(CompleteFlags::DUPLICATES_ARGUMENT)
|
|
);
|
|
let completions = do_complete(L!("cat testfile TE"), CompletionRequestOptions::default());
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("testfile"));
|
|
assert!(completions[0].replaces_token());
|
|
assert!(
|
|
completions[0]
|
|
.flags
|
|
.contains(CompleteFlags::DUPLICATES_ARGUMENT)
|
|
);
|
|
let completions = do_complete(
|
|
L!("something --abc=te"),
|
|
CompletionRequestOptions::default(),
|
|
);
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("stfile"));
|
|
let completions = do_complete(L!("something -abc=te"), CompletionRequestOptions::default());
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("stfile"));
|
|
let completions = do_complete(L!("something abc=te"), CompletionRequestOptions::default());
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("stfile"));
|
|
let completions = do_complete(
|
|
L!("something abc=stfile"),
|
|
CompletionRequestOptions::default(),
|
|
);
|
|
assert_eq!(&completions, &[]);
|
|
let completions = do_complete(L!("something abc=stfile"), fuzzy_options);
|
|
assert_eq!(completions.len(), 1);
|
|
assert_eq!(completions[0].completion, L!("abc=testfile"));
|
|
|
|
// Zero escapes can cause problems. See issue #1631.
|
|
let completions = do_complete(L!("cat foo\\0"), CompletionRequestOptions::default());
|
|
assert_eq!(&completions, &[]);
|
|
let completions = do_complete(L!("cat foo\\0bar"), CompletionRequestOptions::default());
|
|
assert_eq!(&completions, &[]);
|
|
let completions = do_complete(L!("cat \\0"), CompletionRequestOptions::default());
|
|
assert_eq!(&completions, &[]);
|
|
let mut completions = do_complete(L!("cat te\\0"), CompletionRequestOptions::default());
|
|
assert_eq!(&completions, &[]);
|
|
|
|
parser.popd();
|
|
completions.clear();
|
|
|
|
// Test abbreviations.
|
|
parser.eval(
|
|
L!("function testabbrsonetwothreefour; end"),
|
|
&IoChain::new(),
|
|
);
|
|
with_abbrs_mut(|abbrset| {
|
|
abbrset.add(Abbreviation::new(
|
|
L!("somename").into(),
|
|
L!("testabbrsonetwothreezero").into(),
|
|
L!("expansion").into(),
|
|
abbrs::Position::Command,
|
|
false,
|
|
));
|
|
});
|
|
|
|
let completions = complete(
|
|
L!("testabbrsonetwothree"),
|
|
CompletionRequestOptions::default(),
|
|
&parser.context(),
|
|
)
|
|
.0;
|
|
assert_eq!(completions.len(), 2);
|
|
// Abbreviations should not have a space after them.
|
|
assert_eq!(completions[0].completion, L!("zero"));
|
|
assert!(completions[0].flags.contains(CompleteFlags::NO_SPACE));
|
|
with_abbrs_mut(|abbrset| {
|
|
abbrset.erase(L!("testabbrsonetwothreezero"), &[]);
|
|
});
|
|
assert_eq!(completions[1].completion, L!("four"));
|
|
assert!(!completions[1].flags.contains(CompleteFlags::NO_SPACE));
|
|
|
|
// Test wraps.
|
|
assert!(comma_join(complete_get_wrap_targets(L!("wrapper1"))).is_empty());
|
|
complete_add_wrapper(L!("wrapper1").into(), L!("wrapper2").into());
|
|
assert_eq!(
|
|
comma_join(complete_get_wrap_targets(L!("wrapper1"))),
|
|
L!("wrapper2")
|
|
);
|
|
complete_add_wrapper(L!("wrapper2").into(), L!("wrapper3").into());
|
|
assert_eq!(
|
|
comma_join(complete_get_wrap_targets(L!("wrapper1"))),
|
|
L!("wrapper2")
|
|
);
|
|
assert_eq!(
|
|
comma_join(complete_get_wrap_targets(L!("wrapper2"))),
|
|
L!("wrapper3")
|
|
);
|
|
complete_add_wrapper(L!("wrapper3").into(), L!("wrapper1").into()); // loop!
|
|
assert_eq!(
|
|
comma_join(complete_get_wrap_targets(L!("wrapper1"))),
|
|
L!("wrapper2")
|
|
);
|
|
assert_eq!(
|
|
comma_join(complete_get_wrap_targets(L!("wrapper2"))),
|
|
L!("wrapper3")
|
|
);
|
|
assert_eq!(
|
|
comma_join(complete_get_wrap_targets(L!("wrapper3"))),
|
|
L!("wrapper1")
|
|
);
|
|
complete_remove_wrapper(L!("wrapper1").into(), L!("wrapper2"));
|
|
assert!(comma_join(complete_get_wrap_targets(L!("wrapper1"))).is_empty());
|
|
assert_eq!(
|
|
comma_join(complete_get_wrap_targets(L!("wrapper2"))),
|
|
L!("wrapper3")
|
|
);
|
|
assert_eq!(
|
|
comma_join(complete_get_wrap_targets(L!("wrapper3"))),
|
|
L!("wrapper1")
|
|
);
|
|
|
|
// Test cd wrapping chain
|
|
parser.pushd("test/complete_test");
|
|
|
|
complete_add_wrapper(L!("cdwrap1").into(), L!("cd").into());
|
|
complete_add_wrapper(L!("cdwrap2").into(), L!("cdwrap1").into());
|
|
|
|
let mut cd_compl = do_complete(L!("cd "), CompletionRequestOptions::default());
|
|
sort_and_prioritize(&mut cd_compl, CompletionRequestOptions::default());
|
|
|
|
let mut cdwrap1_compl = do_complete(L!("cdwrap1 "), CompletionRequestOptions::default());
|
|
sort_and_prioritize(&mut cdwrap1_compl, CompletionRequestOptions::default());
|
|
|
|
let mut cdwrap2_compl = do_complete(L!("cdwrap2 "), CompletionRequestOptions::default());
|
|
sort_and_prioritize(&mut cdwrap2_compl, CompletionRequestOptions::default());
|
|
|
|
let min_compl_size = cd_compl
|
|
.len()
|
|
.min(cdwrap1_compl.len().min(cdwrap2_compl.len()));
|
|
|
|
assert_eq!(cd_compl.len(), min_compl_size);
|
|
assert_eq!(cdwrap1_compl.len(), min_compl_size);
|
|
assert_eq!(cdwrap2_compl.len(), min_compl_size);
|
|
for i in 0..min_compl_size {
|
|
assert_eq!(cd_compl[i].completion, cdwrap1_compl[i].completion);
|
|
assert_eq!(cdwrap1_compl[i].completion, cdwrap2_compl[i].completion);
|
|
}
|
|
|
|
complete_remove_wrapper(L!("cdwrap1").into(), L!("cd"));
|
|
complete_remove_wrapper(L!("cdwrap2").into(), L!("cdwrap1"));
|
|
parser.popd();
|
|
}
|
|
|
|
// Testing test_autosuggest_suggest_special, in particular for properly handling quotes and
|
|
// backslashes.
|
|
#[test]
|
|
#[serial]
|
|
fn test_autosuggest_suggest_special() {
|
|
let _cleanup = test_init();
|
|
let parser = TestParser::new();
|
|
macro_rules! perform_one_autosuggestion_cd_test {
|
|
($command:literal, $expected:literal, $vars:expr) => {
|
|
let mut comps = complete(
|
|
L!($command),
|
|
CompletionRequestOptions::autosuggest(),
|
|
&OperationContext::background($vars, EXPANSION_LIMIT_BACKGROUND),
|
|
)
|
|
.0;
|
|
|
|
let expects_error = $expected == "<error>";
|
|
|
|
assert_eq!(expects_error, comps.is_empty());
|
|
if !expects_error {
|
|
sort_and_prioritize(&mut comps, CompletionRequestOptions::default());
|
|
let suggestion = &comps[0];
|
|
assert_eq!(suggestion.completion, L!($expected));
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! perform_one_completion_cd_test {
|
|
($command:literal, $expected:literal) => {
|
|
let mut comps = complete(
|
|
L!($command),
|
|
CompletionRequestOptions::default(),
|
|
&OperationContext::foreground(
|
|
&parser,
|
|
Box::new(no_cancel),
|
|
EXPANSION_LIMIT_DEFAULT,
|
|
),
|
|
)
|
|
.0;
|
|
|
|
let expects_error = $expected == "<error>";
|
|
|
|
assert_eq!(expects_error, comps.is_empty());
|
|
if !expects_error {
|
|
sort_and_prioritize(&mut comps, CompletionRequestOptions::default());
|
|
let suggestion = &comps[0];
|
|
assert_eq!(suggestion.completion, L!($expected));
|
|
}
|
|
};
|
|
}
|
|
|
|
std::fs::create_dir_all("test/autosuggest_test/0foobar").unwrap();
|
|
std::fs::create_dir_all("test/autosuggest_test/1foo bar").unwrap();
|
|
std::fs::create_dir_all("test/autosuggest_test/2foo bar").unwrap();
|
|
// Cygwin disallows backslashes in filenames.
|
|
#[cfg(not(cygwin))]
|
|
std::fs::create_dir_all("test/autosuggest_test/3foo\\bar").unwrap();
|
|
// a path with a single quote
|
|
std::fs::create_dir_all("test/autosuggest_test/4foo'bar").unwrap();
|
|
// a path with a double quote
|
|
std::fs::create_dir_all("test/autosuggest_test/5foo\"bar").unwrap();
|
|
// This is to ensure tilde expansion is handled. See the `cd ~/test_autosuggest_suggest_specia`
|
|
// test below.
|
|
// Fake out the home directory
|
|
parser.set_one(
|
|
L!("HOME"),
|
|
ParserEnvSetMode::new(EnvMode::LOCAL | EnvMode::EXPORT),
|
|
L!("test/test-home").to_owned(),
|
|
);
|
|
std::fs::create_dir_all("test/test-home/test_autosuggest_suggest_special/").unwrap();
|
|
std::fs::create_dir_all("test/autosuggest_test/start/unique2/unique3/multi4").unwrap();
|
|
std::fs::create_dir_all("test/autosuggest_test/start/unique2/unique3/multi42").unwrap();
|
|
std::fs::create_dir_all("test/autosuggest_test/start/unique2/.hiddenDir/moreStuff")
|
|
.unwrap();
|
|
|
|
// Ensure symlink don't cause us to chase endlessly.
|
|
// Symbolic link is complicated on Windows/Cygwin (see winsymlinks). The behavior
|
|
// depends on the env var CYGWIN (or MSYS). Currently, the default is to copy
|
|
// the target, which will fail with recursive symlinks
|
|
#[cfg(not(cygwin))]
|
|
{
|
|
std::fs::create_dir_all("test/autosuggest_test/has_loop/loopy").unwrap();
|
|
let _ = std::fs::remove_file("test/autosuggest_test/has_loop/loopy/loop");
|
|
std::os::unix::fs::symlink("../loopy", "test/autosuggest_test/has_loop/loopy/loop")
|
|
.unwrap();
|
|
}
|
|
|
|
let wd = "test/autosuggest_test";
|
|
|
|
let mut vars = PwdEnvironment::default();
|
|
vars.parent.vars.insert(
|
|
L!("HOME").into(),
|
|
parser.vars().get(L!("HOME")).unwrap().as_string(),
|
|
);
|
|
|
|
perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/0", "foobar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/0", "foobar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/0", "foobar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/1", "foo bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/1", "foo bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/1", "foo bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/2", "foo bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/2", "foo bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/2", "foo bar/", &vars);
|
|
#[cfg(not(cygwin))]
|
|
// Windows does not allow backslashes in filenames
|
|
{
|
|
perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/3", "foo\\bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/3", "foo\\bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/3", "foo\\bar/", &vars);
|
|
}
|
|
perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/4", "foo'bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/4", "foo'bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/4", "foo'bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/5", "foo\"bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/5", "foo\"bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/5", "foo\"bar/", &vars);
|
|
|
|
vars.parent
|
|
.vars
|
|
.insert(L!("AUTOSUGGEST_TEST_LOC").to_owned(), str2wcstring(wd));
|
|
perform_one_autosuggestion_cd_test!("cd $AUTOSUGGEST_TEST_LOC/0", "foobar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd ~/test_autosuggest_suggest_specia", "l/", &vars);
|
|
|
|
perform_one_autosuggestion_cd_test!(
|
|
"cd test/autosuggest_test/start/",
|
|
"unique2/unique3/",
|
|
&vars
|
|
);
|
|
|
|
#[cfg(not(cygwin))]
|
|
// We skipped the creation of `loopy/loop` above
|
|
perform_one_autosuggestion_cd_test!(
|
|
"cd test/autosuggest_test/has_loop/",
|
|
"loopy/loop/",
|
|
&vars
|
|
);
|
|
|
|
parser.pushd(wd);
|
|
perform_one_autosuggestion_cd_test!("cd 0", "foobar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd \"0", "foobar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd '0", "foobar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd 1", "foo bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd \"1", "foo bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd '1", "foo bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd 2", "foo bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd \"2", "foo bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd '2", "foo bar/", &vars);
|
|
#[cfg(not(cygwin))]
|
|
// Windows does not allow backslashes in filenames
|
|
{
|
|
perform_one_autosuggestion_cd_test!("cd 3", "foo\\bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd \"3", "foo\\bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd '3", "foo\\bar/", &vars);
|
|
}
|
|
perform_one_autosuggestion_cd_test!("cd 4", "foo'bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd \"4", "foo'bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd '4", "foo'bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd 5", "foo\"bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd \"5", "foo\"bar/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd '5", "foo\"bar/", &vars);
|
|
|
|
// A single quote should defeat tilde expansion.
|
|
perform_one_autosuggestion_cd_test!(
|
|
"cd '~/test_autosuggest_suggest_specia'",
|
|
"<error>",
|
|
&vars
|
|
);
|
|
|
|
// Don't crash on ~ (issue #2696). Note this is cwd dependent.
|
|
std::fs::create_dir_all("~absolutelynosuchuser/path1/path2/").unwrap();
|
|
perform_one_autosuggestion_cd_test!("cd ~absolutelynosuchus", "er/path1/path2/", &vars);
|
|
perform_one_autosuggestion_cd_test!("cd ~absolutelynosuchuser/", "path1/path2/", &vars);
|
|
perform_one_completion_cd_test!("cd ~absolutelynosuchus", "er/");
|
|
perform_one_completion_cd_test!("cd ~absolutelynosuchuser/", "path1/");
|
|
|
|
parser.vars().remove(
|
|
L!("HOME"),
|
|
EnvSetMode::new(EnvMode::LOCAL | EnvMode::EXPORT, false),
|
|
);
|
|
parser.popd();
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn test_autosuggestion_ignores() {
|
|
let _cleanup = test_init();
|
|
// Testing scenarios that should produce no autosuggestions
|
|
macro_rules! perform_one_autosuggestion_should_ignore_test {
|
|
($command:literal) => {
|
|
let comps = complete(
|
|
L!($command),
|
|
CompletionRequestOptions::autosuggest(),
|
|
&OperationContext::empty(),
|
|
)
|
|
.0;
|
|
assert_eq!(&comps, &[]);
|
|
};
|
|
}
|
|
// Do not do file autosuggestions immediately after certain statement terminators - see #1631.
|
|
perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST|");
|
|
perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST&");
|
|
perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST#comment");
|
|
perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST;");
|
|
}
|
|
}
|