Support for command wrapping ("aliases")

Add the --wraps option to 'complete' and 'function'. This allows a
command to (recursively) inherit the completions of a wrapped command.
Fixes #393.

When evaluating a completion, we inspect the entire "wrap chain" for a
command, i.e. we follow the sequence of wrapping until we either hit a
loop (which we silently ignore) or the end of the chain. We then
evaluate completions as if the wrapping command were substituted with
the wrapped command. Currently this only works for commands, i.e.
'complete --command gco --wraps git\ checkout' won't work (that would
seem to encroaching on abbreviations anyways). It might be useful to
show an error message for that case.

The commandline builtin reflects the commandline with the wrapped
command substituted in, so e.g. git completions (which inspect the
command line) will just work. This sort of command line munging is
also performed by 'complete -C' so it's not totally without precedent.

'alias will also now mark its generated function as wrapping the
'target.
This commit is contained in:
ridiculousfish
2014-08-15 18:14:36 -07:00
parent fe68d30be9
commit 06400b83b1
10 changed files with 316 additions and 35 deletions

View File

@@ -1516,7 +1516,7 @@ bool completer_t::complete_param(const wcstring &scmd_orig, const wcstring &spop
if ((o->short_opt == L'\0') && (o->long_opt[0]==L'\0'))
{
use_files &= ((o->result_mode & NO_FILES)==0);
use_files = use_files && ((o->result_mode & NO_FILES)==0);
complete_from_args(str, o->comp, o->localized_desc(), o->flags);
}
@@ -1997,10 +1997,34 @@ void complete(const wcstring &cmd_with_subcmds, std::vector<completion_t> &comps
unescape_string(previous_argument, &previous_argument_unescape, UNESCAPE_DEFAULT) &&
unescape_string(current_argument, &current_argument_unescape, UNESCAPE_INCOMPLETE))
{
do_file = completer.complete_param(current_command_unescape,
// Have to walk over the command and its entire wrap chain
// If any command disables do_file, then they all do
do_file = true;
const wcstring_list_t wrap_chain = complete_get_wrap_chain(current_command_unescape);
for (size_t i=0; i < wrap_chain.size(); i++)
{
// Hackish, this. The first command in the chain is always the given command. For every command past the first, we need to create a transient commandline for builtin_commandline. But not for COMPLETION_REQUEST_AUTOSUGGESTION, which may occur on background threads.
builtin_commandline_scoped_transient_t *transient_cmd = NULL;
if (i == 0)
{
assert(wrap_chain.at(i) == current_command_unescape);
}
else if (! (flags & COMPLETION_REQUEST_AUTOSUGGESTION))
{
assert(cmd_node != NULL);
wcstring faux_cmdline = cmd;
faux_cmdline.replace(cmd_node->source_start, cmd_node->source_length, wrap_chain.at(i));
transient_cmd = new builtin_commandline_scoped_transient_t(faux_cmdline);
}
if (! completer.complete_param(wrap_chain.at(i),
previous_argument_unescape,
current_argument_unescape,
!had_ddash);
!had_ddash))
{
do_file = false;
}
delete transient_cmd; //may be null
}
}
/* If we have found no command specific completions at all, fall back to using file completions. */
@@ -2102,4 +2126,132 @@ void complete_print(wcstring &out)
out.append(L"\n");
}
}
/* Append wraps. This is a wonky interface where even values are the commands, and odd values are the targets that they wrap. */
const wcstring_list_t wrap_pairs = complete_get_wrap_pairs();
assert(wrap_pairs.size() % 2 == 0);
for (size_t i=0; i < wrap_pairs.size(); )
{
const wcstring &cmd = wrap_pairs.at(i++);
const wcstring &target = wrap_pairs.at(i++);
append_format(out, L"complete --command %ls --wraps %ls\n", cmd.c_str(), target.c_str());
}
}
/* Completion "wrapper" support. The map goes from wrapping-command to wrapped-command-list */
static pthread_mutex_t wrapper_lock = PTHREAD_MUTEX_INITIALIZER;
typedef std::map<wcstring, wcstring_list_t> wrapper_map_t;
static wrapper_map_t &wrap_map()
{
ASSERT_IS_LOCKED(wrapper_lock);
// A pointer is a little more efficient than an object as a static because we can elide the thread-safe initialization
static wrapper_map_t *wrapper_map = NULL;
if (wrapper_map == NULL)
{
wrapper_map = new wrapper_map_t();
}
return *wrapper_map;
}
/* Add a new target that is wrapped by command. Example: sgrep (command) wraps grep (target). */
bool complete_add_wrapper(const wcstring &command, const wcstring &new_target)
{
if (command.empty() || new_target.empty())
{
return false;
}
scoped_lock locker(wrapper_lock);
wrapper_map_t &wraps = wrap_map();
wcstring_list_t *targets = &wraps[command];
// If it's already present, we do nothing
if (std::find(targets->begin(), targets->end(), new_target) == targets->end())
{
targets->push_back(new_target);
}
return true;
}
bool complete_remove_wrapper(const wcstring &command, const wcstring &target_to_remove)
{
if (command.empty() || target_to_remove.empty())
{
return false;
}
scoped_lock locker(wrapper_lock);
wrapper_map_t &wraps = wrap_map();
bool result = false;
wrapper_map_t::iterator current_targets_iter = wraps.find(command);
if (current_targets_iter != wraps.end())
{
wcstring_list_t *targets = &current_targets_iter->second;
wcstring_list_t::iterator where = std::find(targets->begin(), targets->end(), target_to_remove);
if (where != targets->end())
{
targets->erase(where);
result = true;
}
}
return result;
}
wcstring_list_t complete_get_wrap_chain(const wcstring &command)
{
if (command.empty())
{
return wcstring_list_t();
}
scoped_lock locker(wrapper_lock);
const wrapper_map_t &wraps = wrap_map();
wcstring_list_t result;
std::set<wcstring> visited; // set of visited commands
wcstring_list_t to_visit(1, command); // stack of remaining-to-visit commands
wcstring target;
while (! to_visit.empty())
{
// Grab the next command to visit, put it in target
target.swap(to_visit.back());
to_visit.pop_back();
// Try inserting into visited. If it was already present, we skip it; this is how we avoid loops.
if (! visited.insert(target).second)
{
continue;
}
// Insert the target in the result. Note this is the command itself, if this is the first iteration of the loop.
result.push_back(target);
// Enqueue its children
wrapper_map_t::const_iterator target_children_iter = wraps.find(target);
if (target_children_iter != wraps.end())
{
const wcstring_list_t &children = target_children_iter->second;
to_visit.insert(to_visit.end(), children.begin(), children.end());
}
}
return result;
}
wcstring_list_t complete_get_wrap_pairs()
{
wcstring_list_t result;
scoped_lock locker(wrapper_lock);
const wrapper_map_t &wraps = wrap_map();
for (wrapper_map_t::const_iterator outer = wraps.begin(); outer != wraps.end(); ++outer)
{
const wcstring &cmd = outer->first;
const wcstring_list_t &targets = outer->second;
for (size_t i=0; i < targets.size(); i++)
{
result.push_back(cmd);
result.push_back(targets.at(i));
}
}
return result;
}