Allow complete to have multiple conditions

This makes it so `complete -c foo -n test1 -n test2` registers *both*
conditions, and when it comes time to check the candidate, tries both,
in that order. If any fails it stops, if all succeed the completion is offered.

The reason for this is that it helps with caching - we have a
condition cache, but conditions like

```fish
test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] length

test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] sub
```

defeats it pretty easily, because the cache only looks at the entire
script as a string - it can't tell that the first `test` is the same
in both.

So this means we separate it into

```fish
complete -f -c string -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] length" -s V -l visible -d "Use the visible width, excluding escape sequences"
+complete -f -c string -n "test (count (commandline -opc)) -ge 2" -n "contains -- (commandline -opc)[2] length" -s V -l visible -d "Use the visible width, excluding escape sequences"
```

which allows the `test` to be cached.

In tests, this improves performance for the string completions by 30%
by reducing all the redundant `test` calls.

The `git` completions can also greatly benefit from this.
This commit is contained in:
Fabian Homborg 2022-05-20 19:02:42 +02:00 committed by Fabian Boehm
parent 5a610f60d7
commit 64b34c8cda
6 changed files with 60 additions and 17 deletions

View file

@ -61,7 +61,7 @@ The following options are available:
Causes the specified command to inherit completions from *WRAPPED_COMMAND* (see below for details).
**-n** or **--condition** *CONDITION*
This completion should only be used if the *CONDITION* (a shell command) returns 0. This makes it possible to specify completions that should only be used in some cases.
This completion should only be used if the *CONDITION* (a shell command) returns 0. This makes it possible to specify completions that should only be used in some cases. If multiple conditions are specified, fish will try them in the order they are specified until one fails or all succeeded.
**-C** or **--do-complete** *STRING*
Makes ``complete`` try to find all possible completions for the specified string. If there is no *STRING*, the current commandline is used instead.

View file

@ -32,7 +32,7 @@
/// Silly function.
static void builtin_complete_add2(const wchar_t *cmd, bool cmd_is_path, const wchar_t *short_opt,
const wcstring_list_t &gnu_opts, const wcstring_list_t &old_opts,
completion_mode_t result_mode, const wchar_t *condition,
completion_mode_t result_mode, const wcstring_list_t &condition,
const wchar_t *comp, const wchar_t *desc, int flags) {
for (const wchar_t *s = short_opt; *s; s++) {
complete_add(cmd, cmd_is_path, wcstring{*s}, option_type_short, result_mode, condition,
@ -59,7 +59,7 @@ static void builtin_complete_add2(const wchar_t *cmd, bool cmd_is_path, const wc
static void builtin_complete_add(const wcstring_list_t &cmds, const wcstring_list_t &paths,
const wchar_t *short_opt, const wcstring_list_t &gnu_opt,
const wcstring_list_t &old_opt, completion_mode_t result_mode,
const wchar_t *condition, const wchar_t *comp, const wchar_t *desc,
const wcstring_list_t &condition, const wchar_t *comp, const wchar_t *desc,
int flags) {
for (const wcstring &cmd : cmds) {
builtin_complete_add2(cmd.c_str(), false /* not path */, short_opt, gnu_opt, old_opt,
@ -135,7 +135,8 @@ maybe_t<int> builtin_complete(parser_t &parser, io_streams_t &streams, const wch
int remove = 0;
wcstring short_opt;
wcstring_list_t gnu_opt, old_opt, subcommand;
const wchar_t *comp = L"", *desc = L"", *condition = L"";
const wchar_t *comp = L"", *desc = L"";
wcstring_list_t condition;
bool do_complete = false;
bool have_do_complete_param = false;
wcstring do_complete_param;
@ -268,8 +269,8 @@ maybe_t<int> builtin_complete(parser_t &parser, io_streams_t &streams, const wch
break;
}
case 'n': {
condition = w.woptarg;
assert(condition);
condition.push_back(w.woptarg);
assert(w.woptarg);
break;
}
case 'w': {
@ -333,12 +334,11 @@ maybe_t<int> builtin_complete(parser_t &parser, io_streams_t &streams, const wch
}
}
if (condition && *condition) {
const wcstring condition_string = condition;
for (const auto &condition_string : condition) {
parse_error_list_t errors;
if (parse_util_detect_errors(condition_string, &errors)) {
streams.err.append_format(L"%ls: Condition '%ls' contained a syntax error", cmd,
condition);
condition_string.c_str());
for (const auto &error : errors) {
streams.err.append_format(L"\n%ls: ", cmd);
streams.err.append(error.describe(condition_string, parser.is_interactive()));
@ -428,7 +428,7 @@ maybe_t<int> builtin_complete(parser_t &parser, io_streams_t &streams, const wch
parser.libdata().builtin_complete_current_commandline = false;
}
} else if (path.empty() && gnu_opt.empty() && short_opt.empty() && old_opt.empty() && !remove &&
!*comp && !*desc && !*condition && wrap_targets.empty() && !result_mode.no_files &&
!*comp && !*desc && condition.empty() && wrap_targets.empty() && !result_mode.no_files &&
!result_mode.force_files && !result_mode.requires_param) {
// No arguments that would add or remove anything specified, so we print the definitions of
// all matching completions.

View file

@ -98,8 +98,8 @@ struct complete_entry_opt_t {
wcstring comp;
// Description of the completion.
wcstring desc;
// Condition under which to use the option.
wcstring condition;
// Conditions under which to use the option.
wcstring_list_t condition;
// Determines how completions should be performed on the argument after the switch.
completion_mode_t result_mode;
// Completion flags.
@ -404,6 +404,7 @@ class completer_t {
bool complete_variable(const wcstring &str, size_t start_offset);
bool condition_test(const wcstring &condition);
bool condition_test(const wcstring_list_t &conditions);
void complete_strings(const wcstring &wc_escaped, const description_func_t &desc_func,
const completion_list_t &possible_comp, complete_flags_t flags);
@ -499,6 +500,13 @@ bool completer_t::condition_test(const wcstring &condition) {
return test_res;
}
bool completer_t::condition_test(const wcstring_list_t &conditions) {
for (const auto &c : conditions) {
if (!condition_test(c)) return false;
}
return true;
}
/// Locate the specified entry. Create it if it doesn't exist. Must be called while locked.
static completion_entry_t &complete_get_exact_entry(completion_entry_set_t &completion_set,
const wcstring &cmd, bool cmd_is_path) {
@ -1725,7 +1733,7 @@ void append_completion(completion_list_t *completions, wcstring comp, wcstring d
void complete_add(const wchar_t *cmd, bool cmd_is_path, const wcstring &option,
complete_option_type_t option_type, completion_mode_t result_mode,
const wchar_t *condition, const wchar_t *comp, const wchar_t *desc,
wcstring_list_t condition, const wchar_t *comp, const wchar_t *desc,
complete_flags_t flags) {
assert(cmd && "Null command");
// option should be empty iff the option type is arguments only.
@ -1742,7 +1750,7 @@ void complete_add(const wchar_t *cmd, bool cmd_is_path, const wcstring &option,
opt.result_mode = result_mode;
if (comp) opt.comp = comp;
if (condition) opt.condition = condition;
opt.condition = std::move(condition);
if (desc) opt.desc = desc;
opt.flags = flags;
@ -1841,7 +1849,9 @@ static wcstring completion2string(const complete_entry_opt_t &o, const wcstring
append_switch(out, L'd', C_(o.desc));
append_switch(out, L'a', o.comp);
append_switch(out, L'n', o.condition);
for (const auto &c : o.condition) {
append_switch(out, L'n', c);
}
out.append(L"\n");
return out;
}

View file

@ -230,7 +230,7 @@ void completions_sort_and_prioritize(completion_list_t *comps,
/// \param flags A set of completion flags
void complete_add(const wchar_t *cmd, bool cmd_is_path, const wcstring &option,
complete_option_type_t option_type, completion_mode_t result_mode,
const wchar_t *condition, const wchar_t *comp, const wchar_t *desc, int flags);
wcstring_list_t condition, const wchar_t *comp, const wchar_t *desc, int flags);
/// Remove a previously defined completion.
void complete_remove(const wcstring &cmd, bool cmd_is_path, const wcstring &option,

View file

@ -3345,7 +3345,7 @@ static void test_complete() {
// Trailing spaces (#1261).
completion_mode_t no_files{};
no_files.no_files = true;
complete_add(L"foobarbaz", false, wcstring(), option_type_args_only, no_files, nullptr, L"qux",
complete_add(L"foobarbaz", false, wcstring(), option_type_args_only, no_files, {}, L"qux",
nullptr, COMPLETE_AUTO_SPACE);
completions = do_complete(L"foobarbaz ", {});
do_test(completions.size() == 1);

View file

@ -466,3 +466,36 @@ complete -C"a=1 b=2 cmd_with_fancy_completion 1 "
complete -c thing -x -F
# CHECKERR: complete: invalid option combination, '--exclusive' and '--force-files'
# Multiple conditions
complete -f -c shot
complete -fc shot -n 'test (count (commandline -opc) -eq 1' -n 'test (commandline -opc)[-1] = shot' -a 'through'
# CHECKERR: complete: Condition 'test (count (commandline -opc) -eq 1' contained a syntax error
# CHECKERR: complete: Unexpected end of string, expecting ')'
# CHECKERR: test (count (commandline -opc) -eq 1
# CHECKERR: ^
complete -fc shot -n 'test (count (commandline -opc)) -eq 1' -n 'test (commandline -opc)[-1] = shot' -a 'through'
complete -fc shot -n 'test (count (commandline -opc)) -eq 2' -n 'test (commandline -opc)[-1] = through' -a 'the'
complete -fc shot -n 'test (count (commandline -opc)) -eq 3' -n 'test (commandline -opc)[-1] = the' -a 'heart'
complete -fc shot -n 'test (count (commandline -opc)) -eq 4' -n 'test (commandline -opc)[-1] = heart' -a 'and'
complete -fc shot -n 'test (count (commandline -opc)) -eq 5' -n 'test (commandline -opc)[-1] = and' -a "you\'re"
complete -fc shot -n 'test (count (commandline -opc)) -eq 6' -n 'test (commandline -opc)[-1] = "you\'re"' -a 'to'
complete -fc shot -n 'test (count (commandline -opc)) -eq 7' -n 'test (commandline -opc)[-1] = to' -a 'blame'
complete -C"shot "
# CHECK: through
complete -C"shot through "
# CHECK: the
# See that conditions after a failing one aren't executed.
set -g oops 0
complete -fc oooops
complete -fc oooops -n true -n true -n true -n 'false' -n 'set -g oops 1' -a oops
complete -C'oooops '
echo $oops
# CHECK: 0
complete -fc oooops -n 'true' -n 'set -g oops 1' -a oops
complete -C'oooops '
# CHECK: oops
echo $oops
# CHECK: 1