Refactor string fuzzy matching

In preparation for introducing "smart case", refactor string fuzzy
matching. Specifically split out the case folding and match type into
separate fields, so that we can introduce more case folding types without
a combinatoric explosion.
This commit is contained in:
ridiculousfish
2020-11-28 13:39:36 -08:00
parent 20b98294ba
commit 4b947e0a23
8 changed files with 223 additions and 244 deletions

View File

@@ -266,33 +266,26 @@ static void unique_completions_retaining_order(completion_list_t *comps) {
}
void completions_sort_and_prioritize(completion_list_t *comps, completion_request_flags_t flags) {
// Find the best match type.
fuzzy_type_t best_type = fuzzy_type_t::none;
if (comps->empty()) return;
// Find the best rank.
uint32_t best_rank = UINT32_MAX;
for (const auto &comp : *comps) {
best_type = std::min(best_type, comp.match.type);
if (best_type <= fuzzy_type_t::prefix) {
// We can't get better than this (see below)
break;
}
}
// If the best type is an exact match, reduce it to prefix match. Otherwise a tab completion
// will only show one match if it matches a file exactly. (see issue #959).
if (best_type == fuzzy_type_t::exact) {
best_type = fuzzy_type_t::prefix;
best_rank = std::min(best_rank, comp.rank());
}
// Throw out completions whose match types are less suitable than the best.
comps->erase(
std::remove_if(comps->begin(), comps->end(),
[&](const completion_t &comp) { return comp.match.type > best_type; }),
comps->end());
// Throw out completions of worse ranks.
comps->erase(std::remove_if(comps->begin(), comps->end(),
[=](const completion_t &comp) { return comp.rank() > best_rank; }),
comps->end());
// Deduplicate both sorted and unsorted results.
unique_completions_retaining_order(comps);
// Sort, provided COMPLETE_DONT_SORT isn't set.
stable_sort(comps->begin(), comps->end(), [](const completion_t &a, const completion_t &b) {
return a.match.type < b.match.type || natural_compare_completions(a, b);
// Here we do not pass suppress_exact, so that exact matches appear first.
stable_sort(comps->begin(), comps->end(), [&](const completion_t &a, const completion_t &b) {
return a.rank() < b.rank() || natural_compare_completions(a, b);
});
// Lastly, if this is for an autosuggestion, prefer to avoid completions that duplicate
@@ -333,12 +326,6 @@ class completer_t {
bool fuzzy() const { return flags & completion_request_t::fuzzy_match; }
fuzzy_type_t max_fuzzy_match_type() const {
// If we are doing fuzzy matching, request all types; if not request only prefix matching.
if (fuzzy()) return fuzzy_type_t::none;
return fuzzy_type_t::prefix_icase;
}
bool try_complete_variable(const wcstring &str);
bool try_complete_user(const wcstring &str);
@@ -1182,16 +1169,15 @@ bool completer_t::complete_variable(const wcstring &str, size_t start_offset) {
bool res = false;
for (const wcstring &env_name : ctx.vars.get_names(0)) {
string_fuzzy_match_t match =
string_fuzzy_match_string(var, env_name, this->max_fuzzy_match_type());
if (match.type == fuzzy_type_t::none) {
continue; // no match
}
bool anchor_start = !fuzzy();
maybe_t<string_fuzzy_match_t> match =
string_fuzzy_match_string(var, env_name, anchor_start);
if (!match) continue;
wcstring comp;
int flags = 0;
complete_flags_t flags = 0;
if (!match_type_requires_full_replacement(match.type)) {
if (!match->requires_full_replacement()) {
// Take only the suffix.
comp.append(env_name.c_str() + varlen);
} else {
@@ -1224,7 +1210,7 @@ bool completer_t::complete_variable(const wcstring &str, size_t start_offset) {
}
// Append matching environment variables
append_completion(&this->completions, std::move(comp), std::move(desc), flags, match);
append_completion(&this->completions, std::move(comp), std::move(desc), flags, *match);
res = true;
}

View File

@@ -78,7 +78,7 @@ class completion_t {
// Construction.
explicit completion_t(wcstring comp, wcstring desc = wcstring(),
string_fuzzy_match_t match = string_fuzzy_match_t(fuzzy_type_t::exact),
string_fuzzy_match_t match = string_fuzzy_match_t::exact_match(),
complete_flags_t flags_val = 0);
completion_t(const completion_t &);
completion_t &operator=(const completion_t &);
@@ -94,9 +94,8 @@ class completion_t {
// example, foo10 is naturally greater than foo2 (but alphabetically less than it).
static bool is_naturally_less_than(const completion_t &a, const completion_t &b);
// Deduplicate a potentially-unsorted vector, preserving the order
template <class Iterator, class HashFunction>
static Iterator unique_unsorted(Iterator begin, Iterator end, HashFunction hash);
/// \return the completion's match rank. Lower ranks are better completions.
uint32_t rank() const { return match.rank(); }
// If this completion replaces the entire token, prepend a prefix. Otherwise do nothing.
void prepend_token_prefix(const wcstring &prefix);
@@ -198,7 +197,7 @@ bool complete_is_valid_argument(const wcstring &str, const wcstring &opt, const
/// \param flags completion flags
void append_completion(completion_list_t *completions, wcstring comp, wcstring desc = wcstring(),
int flags = 0,
string_fuzzy_match_t match = string_fuzzy_match_t(fuzzy_type_t::exact));
string_fuzzy_match_t match = string_fuzzy_match_t::exact_match());
/// Support for "wrap targets." A wrap target is a command that completes like another command.
bool complete_add_wrapper(const wcstring &command, const wcstring &new_target);

View File

@@ -2120,25 +2120,25 @@ static void test_expand() {
static void test_fuzzy_match() {
say(L"Testing fuzzy string matching");
// Check that a string fuzzy match has the expected type and case folding.
using type_t = string_fuzzy_match_t::contain_type_t;
using case_fold_t = string_fuzzy_match_t::case_fold_t;
auto test_fuzzy = [](const wchar_t *inp, const wchar_t *exp, type_t type,
case_fold_t fold) -> bool {
auto m = string_fuzzy_match_string(inp, exp);
return m && m->type == type && m->case_fold == fold;
};
if (string_fuzzy_match_string(L"", L"").type != fuzzy_type_t::exact)
err(L"test_fuzzy_match failed on line %ld", __LINE__);
if (string_fuzzy_match_string(L"alpha", L"alpha").type != fuzzy_type_t::exact)
err(L"test_fuzzy_match failed on line %ld", __LINE__);
if (string_fuzzy_match_string(L"alp", L"alpha").type != fuzzy_type_t::prefix)
err(L"test_fuzzy_match failed on line %ld", __LINE__);
if (string_fuzzy_match_string(L"ALPHA!", L"alPhA!").type != fuzzy_type_t::exact_icase)
err(L"test_fuzzy_match failed on line %ld", __LINE__);
if (string_fuzzy_match_string(L"alPh", L"ALPHA!").type != fuzzy_type_t::prefix_icase)
err(L"test_fuzzy_match failed on line %ld", __LINE__);
if (string_fuzzy_match_string(L"LPH", L"ALPHA!").type != fuzzy_type_t::substr)
err(L"test_fuzzy_match failed on line %ld", __LINE__);
if (string_fuzzy_match_string(L"lPh", L"ALPHA!").type != fuzzy_type_t::substr_icase)
err(L"test_fuzzy_match failed on line %ld", __LINE__);
if (string_fuzzy_match_string(L"AA", L"ALPHA!").type != fuzzy_type_t::subseq)
err(L"test_fuzzy_match failed on line %ld", __LINE__);
if (string_fuzzy_match_string(L"BB", L"ALPHA!").type != fuzzy_type_t::none)
err(L"test_fuzzy_match failed on line %ld", __LINE__);
do_test(test_fuzzy(L"", L"", type_t::exact, case_fold_t::samecase));
do_test(test_fuzzy(L"alpha", L"alpha", type_t::exact, case_fold_t::samecase));
do_test(test_fuzzy(L"alp", L"alpha", type_t::prefix, case_fold_t::samecase));
do_test(test_fuzzy(L"ALPHA!", L"alPhA!", type_t::exact, case_fold_t::icase));
do_test(test_fuzzy(L"alPh", L"ALPHA!", type_t::prefix, case_fold_t::icase));
do_test(test_fuzzy(L"LPH", L"ALPHA!", type_t::substr, case_fold_t::samecase));
do_test(test_fuzzy(L"lPh", L"ALPHA!", type_t::substr, case_fold_t::icase));
do_test(test_fuzzy(L"AA", L"ALPHA!", type_t::subseq, case_fold_t::samecase));
do_test(!string_fuzzy_match_string(L"lh", L"ALPHA!").has_value()); // no subseq icase
do_test(!string_fuzzy_match_string(L"BB", L"ALPHA!").has_value());
}
static void test_ifind() {

View File

@@ -350,17 +350,14 @@ bool pager_t::completion_info_passes_filter(const comp_t &info) const {
const wcstring &needle = this->search_field_line.text();
// We do full fuzzy matching just like the completion code itself.
const fuzzy_type_t limit = fuzzy_type_t::none;
// Match against the description.
if (string_fuzzy_match_string(needle, info.desc, limit).type != fuzzy_type_t::none) {
if (string_fuzzy_match_string(needle, info.desc)) {
return true;
}
// Match against the completion strings.
for (const auto &i : info.comp) {
if (string_fuzzy_match_string(needle, prefix + i, limit).type != fuzzy_type_t::none) {
if (string_fuzzy_match_string(needle, prefix + i)) {
return true;
}
}

View File

@@ -1805,18 +1805,13 @@ static bool reader_can_replace(const wcstring &in, int flags) {
return true;
}
/// Determine the best match type for a set of completions.
static fuzzy_type_t get_best_match_type(const completion_list_t &comp) {
fuzzy_type_t best_type = fuzzy_type_t::none;
for (const auto &i : comp) {
best_type = std::min(best_type, i.match.type);
/// Determine the best (lowest) match rank for a set of completions.
static uint32_t get_best_rank(const completion_list_t &comp) {
uint32_t best_rank = UINT32_MAX;
for (const auto &c : comp) {
best_rank = std::min(best_rank, c.rank());
}
// If the best type is an exact match, reduce it to prefix match. Otherwise a tab completion
// will only show one match if it matches a file exactly. (see issue #959).
if (best_type == fuzzy_type_t::exact) {
best_type = fuzzy_type_t::prefix;
}
return best_type;
return best_rank;
}
/// Handle the list of completions. This means the following:
@@ -1865,13 +1860,13 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok
return success;
}
fuzzy_type_t best_match_type = get_best_match_type(comp);
auto best_rank = get_best_rank(comp);
// Determine whether we are going to replace the token or not. If any commands of the best
// type do not require replacement, then ignore all those that want to use replacement.
// rank do not require replacement, then ignore all those that want to use replacement.
bool will_replace_token = true;
for (const completion_t &el : comp) {
if (el.match.type <= best_match_type && !(el.flags & COMPLETE_REPLACES_TOKEN)) {
if (el.rank() <= best_rank && !(el.flags & COMPLETE_REPLACES_TOKEN)) {
will_replace_token = false;
break;
}
@@ -1880,9 +1875,10 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok
// Decide which completions survived. There may be a lot of them; it would be nice if we could
// figure out how to avoid copying them here.
completion_list_t surviving_completions;
bool all_matches_exact_or_prefix = true;
for (const completion_t &el : comp) {
// Ignore completions with a less suitable match type than the best.
if (el.match.type > best_match_type) continue;
// Ignore completions with a less suitable match rank than the best.
if (el.rank() > best_rank) continue;
// Only use completions that match replace_token.
bool completion_replace_token = static_cast<bool>(el.flags & COMPLETE_REPLACES_TOKEN);
@@ -1893,11 +1889,12 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok
// This completion survived.
surviving_completions.push_back(el);
all_matches_exact_or_prefix = all_matches_exact_or_prefix && el.match.is_exact_or_prefix();
}
bool use_prefix = false;
wcstring common_prefix;
if (match_type_shares_prefix(best_match_type)) {
if (all_matches_exact_or_prefix) {
// Try to find a common prefix to insert among the surviving completions.
complete_flags_t flags = 0;
bool prefix_is_partial_completion = false;
@@ -1959,7 +1956,7 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok
// Print the completion list.
wcstring prefix;
if (will_replace_token || match_type_requires_full_replacement(best_match_type)) {
if (will_replace_token || !all_matches_exact_or_prefix) {
if (use_prefix) prefix = std::move(common_prefix);
} else if (tok.size() + common_prefix.size() <= PREFIX_MAX_LEN) {
prefix = tok + common_prefix;

View File

@@ -154,52 +154,68 @@ static bool subsequence_in_string(const wcstring &needle, const wcstring &haysta
return ni == needle.end();
}
string_fuzzy_match_t::string_fuzzy_match_t(enum fuzzy_type_t t, size_t distance_first,
size_t distance_second)
: type(t), match_distance_first(distance_first), match_distance_second(distance_second) {}
string_fuzzy_match_t string_fuzzy_match_string(const wcstring &string,
const wcstring &match_against,
fuzzy_type_t limit_type) {
// Distances are generally the amount of text not matched.
string_fuzzy_match_t result(fuzzy_type_t::none, 0, 0);
size_t location;
if (limit_type >= fuzzy_type_t::exact && string == match_against) {
result.type = fuzzy_type_t::exact;
} else if (limit_type >= fuzzy_type_t::prefix &&
string_prefixes_string(string, match_against)) {
result.type = fuzzy_type_t::prefix;
assert(match_against.size() >= string.size());
result.match_distance_first = match_against.size() - string.size();
} else if (limit_type >= fuzzy_type_t::exact_icase &&
wcscasecmp(string.c_str(), match_against.c_str()) == 0) {
result.type = fuzzy_type_t::exact_icase;
} else if (limit_type >= fuzzy_type_t::prefix_icase &&
string_prefixes_string_case_insensitive(string, match_against)) {
result.type = fuzzy_type_t::prefix_icase;
assert(match_against.size() >= string.size());
result.match_distance_first = match_against.size() - string.size();
} else if (limit_type >= fuzzy_type_t::substr &&
(location = match_against.find(string)) != wcstring::npos) {
// String is contained within match against.
result.type = fuzzy_type_t::substr;
assert(match_against.size() >= string.size());
result.match_distance_first = match_against.size() - string.size();
result.match_distance_second = location; // prefer earlier matches
} else if (limit_type >= fuzzy_type_t::substr_icase &&
(location = ifind(match_against, string, true)) != wcstring::npos) {
// A case-insensitive version of the string is in the match against.
result.type = fuzzy_type_t::substr_icase;
assert(match_against.size() >= string.size());
result.match_distance_first = match_against.size() - string.size();
result.match_distance_second = location; // prefer earlier matches
} else if (limit_type >= fuzzy_type_t::subseq && subsequence_in_string(string, match_against)) {
result.type = fuzzy_type_t::subseq;
assert(match_against.size() >= string.size());
result.match_distance_first = match_against.size() - string.size();
// It would be nice to prefer matches with greater matching runs here.
// static
maybe_t<string_fuzzy_match_t> string_fuzzy_match_t::try_create(const wcstring &string,
const wcstring &match_against,
bool anchor_start) {
// A string cannot fuzzy match against a shorter string.
if (string.size() > match_against.size()) {
return none();
}
return result;
// exact samecase
if (string == match_against) {
return string_fuzzy_match_t{contain_type_t::exact, case_fold_t::samecase};
}
// prefix samecase
if (string_prefixes_string(string, match_against)) {
return string_fuzzy_match_t{contain_type_t::prefix, case_fold_t::samecase};
}
// exact icase
if (wcscasecmp(string.c_str(), match_against.c_str()) == 0) {
return string_fuzzy_match_t{contain_type_t::exact, case_fold_t::icase};
}
// prefix icase
if (string_prefixes_string_case_insensitive(string, match_against)) {
return string_fuzzy_match_t{contain_type_t::prefix, case_fold_t::icase};
}
// If anchor_start is set, this is as far as we go.
if (anchor_start) {
return none();
}
// substr samecase
size_t location;
if ((location = match_against.find(string)) != wcstring::npos) {
return string_fuzzy_match_t{contain_type_t::substr, case_fold_t::samecase};
}
// substr icase
if ((location = ifind(match_against, string, true /* fuzzy */)) != wcstring::npos) {
return string_fuzzy_match_t{contain_type_t::substr, case_fold_t::icase};
}
// subseq samecase
if (subsequence_in_string(string, match_against)) {
return string_fuzzy_match_t{contain_type_t::subseq, case_fold_t::samecase};
}
// We do not currently test subseq icase.
return none();
}
uint32_t string_fuzzy_match_t::rank() const {
// Combine our type and our case fold into a single number, such that better matches are
// smaller. Treat 'exact' types the same as 'prefix' types; this is because we do not
// prefer exact matches to prefix matches when presenting completions to the user.
auto effective_type = (type == contain_type_t::exact ? contain_type_t::prefix : type);
// Type dominates fold.
return static_cast<uint32_t>(effective_type) * 8 + static_cast<uint32_t>(case_fold);
}
template <bool Fuzzy, typename T>

View File

@@ -35,89 +35,86 @@ bool string_prefixes_string_case_insensitive(const wcstring &proposed_prefix,
size_t ifind(const wcstring &haystack, const wcstring &needle, bool fuzzy = false);
size_t ifind(const std::string &haystack, const std::string &needle, bool fuzzy = false);
// Ways that a string may fuzzily match another.
enum class fuzzy_type_t {
// We match the string exactly: FOOBAR matches FOOBAR.
exact,
// We match a prefix of the string: FO matches FOOBAR.
prefix,
// We match the string exactly, but in a case insensitive way: foobar matches FOOBAR.
exact_icase,
// We match a prefix of the string, in a case insensitive way: foo matches FOOBAR.
prefix_icase,
// We match a substring of the string: OOBA matches FOOBAR.
substr,
// We match a substring of the string: ooBA matches FOOBAR.
substr_icase,
// A subsequence match with insertions only: FBR matches FOOBAR.
subseq,
// We don't match the string.
none,
};
/// Indicates where a match type requires replacing the entire token.
static inline bool match_type_requires_full_replacement(fuzzy_type_t t) {
switch (t) {
case fuzzy_type_t::exact:
case fuzzy_type_t::prefix:
return false;
case fuzzy_type_t::exact_icase:
case fuzzy_type_t::prefix_icase:
case fuzzy_type_t::substr:
case fuzzy_type_t::substr_icase:
case fuzzy_type_t::subseq:
case fuzzy_type_t::none:
return true;
}
DIE("Unreachable");
return false;
}
/// Indicates where a match shares a prefix with the string it matches.
static inline bool match_type_shares_prefix(fuzzy_type_t t) {
switch (t) {
case fuzzy_type_t::exact:
case fuzzy_type_t::prefix:
case fuzzy_type_t::exact_icase:
case fuzzy_type_t::prefix_icase:
return true;
case fuzzy_type_t::substr:
case fuzzy_type_t::substr_icase:
case fuzzy_type_t::subseq:
case fuzzy_type_t::none:
return false;
}
DIE("Unreachable");
return false;
}
/// Test if string is a fuzzy match to another.
/// A lightweight value-type describing how closely a string fuzzy-matches another string.
struct string_fuzzy_match_t {
enum fuzzy_type_t type;
// The ways one string can contain another.
enum class contain_type_t : uint8_t {
exact, // exact match: foobar matches foo
prefix, // prefix match: foo matches foobar
substr, // substring match: ooba matches foobar
subseq, // subsequence match: fbr matches foobar
};
contain_type_t type;
// Strength of the match. The value depends on the type. Lower is stronger.
size_t match_distance_first;
size_t match_distance_second;
// The case-folding required for the match.
enum class case_fold_t : uint8_t {
samecase, // exact match: foobar matches foobar
icase, // case insensitive: FoBaR matches foobar
};
case_fold_t case_fold;
// Constructor.
explicit string_fuzzy_match_t(enum fuzzy_type_t t, size_t distance_first = 0,
size_t distance_second = 0);
constexpr string_fuzzy_match_t(contain_type_t type, case_fold_t case_fold)
: type(type), case_fold(case_fold) {}
// Helper to return an exact match.
static constexpr string_fuzzy_match_t exact_match() {
return string_fuzzy_match_t(contain_type_t::exact, case_fold_t::samecase);
}
/// \return whether this is a samecase exact match.
bool is_samecase_exact() const {
return type == contain_type_t::exact && case_fold == case_fold_t::samecase;
}
/// \return if we are exact or prefix match.
bool is_exact_or_prefix() const {
switch (type) {
case contain_type_t::exact:
case contain_type_t::prefix:
return true;
case contain_type_t::substr:
case contain_type_t::subseq:
return false;
}
DIE("Unreachable");
return false;
}
// \return if our match requires a full replacement, i.e. is not a strict extension of our
// existing string. This is false only if our case matches, and our type is prefix or exact.
bool requires_full_replacement() const {
if (case_fold != case_fold_t::samecase) return true;
switch (type) {
case contain_type_t::exact:
case contain_type_t::prefix:
return false;
case contain_type_t::substr:
case contain_type_t::subseq:
return true;
}
DIE("Unreachable");
return false;
}
/// Try creating a fuzzy match for \p string against \p match_against.
/// \p string is something like "foo" and \p match_against is like "FooBar".
/// If \p anchor_start is set, then only exact and prefix matches are permitted.
static maybe_t<string_fuzzy_match_t> try_create(const wcstring &string,
const wcstring &match_against,
bool anchor_start);
/// \return a rank for filtering matches.
/// Earlier (smaller) ranks are better matches.
uint32_t rank() const;
};
/// Compute a fuzzy match for a string. If maximum_match is not fuzzy_match_t::none, limit the type
/// to matches at or below that type.
string_fuzzy_match_t string_fuzzy_match_string(const wcstring &string,
const wcstring &match_against,
fuzzy_type_t limit_type = fuzzy_type_t::none);
/// Cover over string_fuzzy_match_t::try_create().
inline maybe_t<string_fuzzy_match_t> string_fuzzy_match_string(const wcstring &string,
const wcstring &match_against,
bool anchor_start = false) {
return string_fuzzy_match_t::try_create(string, match_against, anchor_start);
}
/// Split a string by a separator character.
wcstring_list_t split_string(const wcstring &val, wchar_t sep);

View File

@@ -98,14 +98,13 @@ bool wildcard_has(const wcstring &str, bool internal) {
/// \param wc The wildcard.
/// \param leading_dots_fail_to_match Whether files beginning with dots should not be matched
/// against wildcards.
static enum fuzzy_type_t wildcard_match_internal(const wcstring &str, const wcstring &wc,
bool leading_dots_fail_to_match) {
bool wildcard_match(const wcstring &str, const wcstring &wc, bool leading_dots_fail_to_match) {
// Hackish fix for issue #270. Prevent wildcards from matching . or .., but we must still allow
// literal matches.
if (leading_dots_fail_to_match && str[0] == L'.' &&
(str[1] == L'\0' || (str[1] == L'.' && str[2] == L'\0'))) {
// The string is '.' or '..' so the only possible match is an exact match.
return str == wc ? fuzzy_type_t::exact : fuzzy_type_t::none;
return str == wc;
}
// Near Linear implementation as proposed here https://research.swtch.com/glob.
@@ -121,13 +120,13 @@ static enum fuzzy_type_t wildcard_match_internal(const wcstring &str, const wcst
if (*wc_x == ANY_STRING || *wc_x == ANY_STRING_RECURSIVE) {
// Ignore hidden file
if (leading_dots_fail_to_match && is_first && str[0] == L'.') {
return fuzzy_type_t::none;
return false;
}
// Common case of * at the end. In that case we can early out since we know it will
// match.
if (wc_x[1] == L'\0') {
return fuzzy_type_t::exact;
return true;
}
// Try to match at str_x.
// If that doesn't work out, restart at str_x+1 next.
@@ -138,7 +137,7 @@ static enum fuzzy_type_t wildcard_match_internal(const wcstring &str, const wcst
continue;
} else if (*wc_x == ANY_CHAR && *str_x != 0) {
if (is_first && *str_x == L'.') {
return fuzzy_type_t::none;
return false;
}
wc_x++;
str_x++;
@@ -155,10 +154,10 @@ static enum fuzzy_type_t wildcard_match_internal(const wcstring &str, const wcst
str_x = restart_str_x;
continue;
}
return fuzzy_type_t::none;
return false;
}
// Matched all of pattern to all of name. Success.
return fuzzy_type_t::exact;
return true;
}
// This does something horrible refactored from an even more horrible function.
@@ -192,7 +191,9 @@ static bool has_prefix_match(const completion_list_t *comps, size_t first) {
if (comps != nullptr) {
const size_t after_count = comps->size();
for (size_t j = first; j < after_count; j++) {
if (comps->at(j).match.type <= fuzzy_type_t::prefix) {
const auto &match = comps->at(j).match;
if (match.type <= string_fuzzy_match_t::contain_type_t::prefix &&
match.case_fold == string_fuzzy_match_t::case_fold_t::samecase) {
return true;
}
}
@@ -234,29 +235,24 @@ static bool wildcard_complete_internal(const wchar_t *const str, size_t str_len,
// Maybe we have no more wildcards at all. This includes the empty string.
if (next_wc_char_pos == wcstring::npos) {
// A string cannot fuzzy match a pattern without wildcards that is longer than the string
// itself
if (wc_len > str_len) {
// Try matching.
maybe_t<string_fuzzy_match_t> match = string_fuzzy_match_string(wc, str);
if (!match) return false;
// If we're not allowing fuzzy match, then we require a prefix match.
bool needs_prefix_match = !(params.expand_flags & expand_flag::fuzzy_match);
if (needs_prefix_match && !match->is_exact_or_prefix()) {
return false;
}
auto match = string_fuzzy_match_string(wc, str);
// If we're allowing fuzzy match, any match is OK. Otherwise we require a prefix match.
bool match_acceptable;
if (params.expand_flags & expand_flag::fuzzy_match) {
match_acceptable = match.type != fuzzy_type_t::none;
} else {
match_acceptable = match_type_shares_prefix(match.type);
}
if (!match_acceptable || out == nullptr) {
return match_acceptable;
// The match was successful. If the string is not requested we're done.
if (out == nullptr) {
return true;
}
// Wildcard complete.
bool full_replacement =
match_type_requires_full_replacement(match.type) || (flags & COMPLETE_REPLACES_TOKEN);
match->requires_full_replacement() || (flags & COMPLETE_REPLACES_TOKEN);
// If we are not replacing the token, be careful to only store the part of the string after
// the wildcard.
@@ -268,8 +264,8 @@ static bool wildcard_complete_internal(const wchar_t *const str, size_t str_len,
// Note: out_completion may be empty if the completion really is empty, e.g. tab-completing
// 'foo' when a file 'foo' exists.
complete_flags_t local_flags = flags | (full_replacement ? COMPLETE_REPLACES_TOKEN : 0);
append_completion(out, out_completion, out_desc, local_flags, match);
return match_acceptable;
append_completion(out, out_completion, out_desc, local_flags, *match);
return true;
} else if (next_wc_char_pos > 0) {
// The literal portion of a wildcard cannot be longer than the string itself,
// e.g. `abc*` can never match a string that is only two characters long.
@@ -355,11 +351,6 @@ bool wildcard_complete(const wcstring &str, const wchar_t *wc,
out, true /* first call */);
}
bool wildcard_match(const wcstring &str, const wcstring &wc, bool leading_dots_fail_to_match) {
enum fuzzy_type_t match = wildcard_match_internal(str, wc, leading_dots_fail_to_match);
return match != fuzzy_type_t::none;
}
static int fast_waccess(const struct stat &stat_buf, uint8_t mode) {
// Cache the effective user id and group id of our own shell process. These can't change on us
// because we don't change them.
@@ -816,10 +807,8 @@ void wildcard_expander_t::expand_literal_intermediate_segment_with_fuzz(const wc
// Skip cases that don't match or match exactly. The match-exactly case was handled directly
// in expand().
const string_fuzzy_match_t match = string_fuzzy_match_string(wc_segment, name_str);
if (match.type == fuzzy_type_t::none || match.type == fuzzy_type_t::exact) {
continue;
}
const maybe_t<string_fuzzy_match_t> match = string_fuzzy_match_string(wc_segment, name_str);
if (!match || match->is_samecase_exact()) continue;
wcstring new_full_path = base_dir + name_str;
new_full_path.push_back(L'/');
@@ -829,12 +818,10 @@ void wildcard_expander_t::expand_literal_intermediate_segment_with_fuzz(const wc
continue;
}
// Determine the effective prefix for our children
// Determine the effective prefix for our children.
// Normally this would be the wildcard segment, but here we know our segment doesn't have
// wildcards
// ("literal") and we are doing fuzzy expansion, which means we replace the segment with
// files found
// through fuzzy matching
// wildcards ("literal") and we are doing fuzzy expansion, which means we replace the
// segment with files found through fuzzy matching.
const wcstring child_prefix = prefix + name_str + L'/';
// Ok, this directory matches. Recurse to it. Then mark each resulting completion as fuzzy.
@@ -852,9 +839,9 @@ void wildcard_expander_t::expand_literal_intermediate_segment_with_fuzz(const wc
}
// And every match must be made at least as fuzzy as ours.
// TODO: justify this, tests do not exercise it yet.
if (match.type > c->match.type) {
if (match->rank() > c->match.rank()) {
// Our match is fuzzier.
c->match = match;
c->match = *match;
}
}
}