Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shell completion improvements #6128

Merged
merged 7 commits into from
Apr 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion misc/bash/completion.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function _complete_nix {
else
COMPREPLY+=("$completion")
fi
done < <(NIX_GET_COMPLETIONS=$cword "${words[@]/#\~/$HOME}" 2>/dev/null)
done < <(NIX_GET_COMPLETIONS=$cword "${words[@]}" 2>/dev/null)
__ltrim_colon_completions "$cur"
}

Expand Down
2 changes: 1 addition & 1 deletion src/libcmd/command.hh
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ struct InstallableCommand : virtual Args, SourceExprCommand

std::optional<FlakeRef> getFlakeRefForCompletion() override
{
return parseFlakeRef(_installable, absPath("."));
return parseFlakeRefWithFragment(_installable, absPath(".")).first;
}

private:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "registry.hh"
#include "flake/flakeref.hh"
#include "store-api.hh"
#include "command.hh"

namespace nix {

Expand Down Expand Up @@ -59,6 +60,9 @@ MixEvalArgs::MixEvalArgs()
fetchers::Attrs extraAttrs;
if (to.subdir != "") extraAttrs["dir"] = to.subdir;
fetchers::overrideRegistry(from.input, to.input, extraAttrs);
}},
.completer = {[&](size_t, std::string_view prefix) {
completeFlakeRef(openStore(), prefix);
}}
});

Expand Down
File renamed without changes.
29 changes: 20 additions & 9 deletions src/libcmd/installables.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "installables.hh"
#include "util.hh"
#include "command.hh"
#include "attr-path.hh"
#include "common-eval-args.hh"
Expand Down Expand Up @@ -98,6 +99,14 @@ MixFlakeOptions::MixFlakeOptions()
lockFlags.inputOverrides.insert_or_assign(
flake::parseInputPath(inputPath),
parseFlakeRef(flakeRef, absPath("."), true));
}},
.completer = {[&](size_t n, std::string_view prefix) {
if (n == 0) {
if (auto flakeRef = getFlakeRefForCompletion())
completeFlakeInputPath(getEvalState(), *flakeRef, prefix);
} else if (n == 1) {
completeFlakeRef(getEvalState()->store, prefix);
}
Comment on lines +103 to +109
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noting that this only works if --override-input is given after the installable. making it work before the installable may require bigger changes so out of scope for this pr as its already an improvement

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #6693, but note that this is strongly limited. For example, nix build --override-input foo<Tab> my-flake will treat my-flake as the second argument to --override-input and there's not much we can do about that.

}}
});

Expand Down Expand Up @@ -179,6 +188,8 @@ Strings SourceExprCommand::getDefaultFlakeAttrPathPrefixes()
void SourceExprCommand::completeInstallable(std::string_view prefix)
{
if (file) {
completionType = ctAttrs;

evalSettings.pureEval = false;
auto state = getEvalState();
Expr *e = state->parseExprFromFile(
Expand Down Expand Up @@ -207,13 +218,14 @@ void SourceExprCommand::completeInstallable(std::string_view prefix)
Value v2;
state->autoCallFunction(*autoArgs, v1, v2);

completionType = ctAttrs;

if (v2.type() == nAttrs) {
for (auto & i : *v2.attrs) {
std::string name = i.name;
if (name.find(searchWord) == 0) {
completions->add(i.name);
if (prefix_ == "")
completions->add(name);
else
completions->add(prefix_ + "." + name);
}
}
}
Expand Down Expand Up @@ -241,10 +253,11 @@ void completeFlakeRefWithFragment(
if (hash == std::string::npos) {
completeFlakeRef(evalState->store, prefix);
} else {
completionType = ctAttrs;

auto fragment = prefix.substr(hash + 1);
auto flakeRefS = std::string(prefix.substr(0, hash));
// FIXME: do tilde expansion.
auto flakeRef = parseFlakeRef(flakeRefS, absPath("."));
auto flakeRef = parseFlakeRef(expandTilde(flakeRefS), absPath("."));

auto evalCache = openEvalCache(*evalState,
std::make_shared<flake::LockedFlake>(lockFlake(*evalState, flakeRef, lockFlags)));
Expand All @@ -256,8 +269,6 @@ void completeFlakeRefWithFragment(
flake. */
attrPathPrefixes.push_back("");

completionType = ctAttrs;

for (auto & attrPathPrefixS : attrPathPrefixes) {
auto attrPathPrefix = parseAttrPath(*evalState, attrPathPrefixS);
auto attrPathS = attrPathPrefixS + std::string(fragment);
Expand Down Expand Up @@ -905,10 +916,10 @@ std::optional<FlakeRef> InstallablesCommand::getFlakeRefForCompletion()
{
if (_installables.empty()) {
if (useDefaultInstallables())
return parseFlakeRef(".", absPath("."));
return parseFlakeRefWithFragment(".", absPath(".")).first;
return {};
}
return parseFlakeRef(_installables.front(), absPath("."));
return parseFlakeRefWithFragment(_installables.front(), absPath(".")).first;
}

InstallableCommand::InstallableCommand()
Expand Down
36 changes: 20 additions & 16 deletions src/libutil/args.cc
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ bool Args::processFlag(Strings::iterator & pos, Strings::iterator end)
if (flag.handler.arity == ArityAny) break;
throw UsageError("flag '%s' requires %d argument(s)", name, flag.handler.arity);
}
if (flag.completer)
if (auto prefix = needsCompletion(*pos)) {
anyCompleted = true;
if (auto prefix = needsCompletion(*pos)) {
anyCompleted = true;
if (flag.completer)
flag.completer(n, *prefix);
}
}
args.push_back(*pos++);
}
if (!anyCompleted)
Expand All @@ -146,6 +146,7 @@ bool Args::processFlag(Strings::iterator & pos, Strings::iterator end)
&& hasPrefix(name, std::string(*prefix, 2)))
completions->add("--" + name, flag->description);
}
return false;
}
auto i = longFlags.find(std::string(*pos, 2));
if (i == longFlags.end()) return false;
Expand Down Expand Up @@ -187,10 +188,12 @@ bool Args::processArgs(const Strings & args, bool finish)
{
std::vector<std::string> ss;
for (const auto &[n, s] : enumerate(args)) {
ss.push_back(s);
if (exp.completer)
if (auto prefix = needsCompletion(s))
if (auto prefix = needsCompletion(s)) {
ss.push_back(*prefix);
if (exp.completer)
exp.completer(n, *prefix);
} else
ss.push_back(s);
}
exp.handler.fun(ss);
expectedArgs.pop_front();
Expand Down Expand Up @@ -279,21 +282,22 @@ static void _completePath(std::string_view prefix, bool onlyDirs)
{
completionType = ctFilenames;
glob_t globbuf;
int flags = GLOB_NOESCAPE | GLOB_TILDE;
int flags = GLOB_NOESCAPE;
#ifdef GLOB_ONLYDIR
if (onlyDirs)
flags |= GLOB_ONLYDIR;
#endif
if (glob((std::string(prefix) + "*").c_str(), flags, nullptr, &globbuf) == 0) {
// using expandTilde here instead of GLOB_TILDE(_CHECK) so that ~<Tab> expands to /home/user/
if (glob((expandTilde(prefix) + "*").c_str(), flags, nullptr, &globbuf) == 0) {
for (size_t i = 0; i < globbuf.gl_pathc; ++i) {
if (onlyDirs) {
auto st = lstat(globbuf.gl_pathv[i]);
auto st = stat(globbuf.gl_pathv[i]);
if (!S_ISDIR(st.st_mode)) continue;
}
completions->add(globbuf.gl_pathv[i]);
}
globfree(&globbuf);
}
globfree(&globbuf);
}

void completePath(size_t, std::string_view prefix)
Expand Down Expand Up @@ -322,16 +326,16 @@ MultiCommand::MultiCommand(const Commands & commands_)
.optional = true,
.handler = {[=](std::string s) {
assert(!command);
if (auto prefix = needsCompletion(s)) {
for (auto & [name, command] : commands)
if (hasPrefix(name, *prefix))
completions->add(name);
}
auto i = commands.find(s);
if (i == commands.end())
throw UsageError("'%s' is not a recognised command", s);
command = {s, i->second()};
command->second->parent = this;
}},
.completer = {[&](size_t, std::string_view prefix) {
for (auto & [name, command] : commands)
if (hasPrefix(name, prefix))
completions->add(name);
}}
});

Expand Down
20 changes: 20 additions & 0 deletions src/libutil/util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,17 @@ std::string_view baseNameOf(std::string_view path)
}


std::string expandTilde(std::string_view path)
{
// TODO: expand ~user ?
auto tilde = path.substr(0, 2);
if (tilde == "~/" || tilde == "~")
return getHome() + std::string(path.substr(1));
else
return std::string(path);
}


bool isInDir(std::string_view path, std::string_view dir)
{
return path.substr(0, 1) == "/"
Expand All @@ -215,6 +226,15 @@ bool isDirOrInDir(std::string_view path, std::string_view dir)
}


struct stat stat(const Path & path)
{
struct stat st;
if (stat(path.c_str(), &st))
throw SysError("getting status of '%1%'", path);
return st;
}


struct stat lstat(const Path & path)
{
struct stat st;
Expand Down
4 changes: 4 additions & 0 deletions src/libutil/util.hh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ Path dirOf(const PathView path);
following the final `/' (trailing slashes are removed). */
std::string_view baseNameOf(std::string_view path);

/* Perform tilde expansion on a path. */
std::string expandTilde(std::string_view path);

/* Check whether 'path' is a descendant of 'dir'. Both paths must be
canonicalized. */
bool isInDir(std::string_view path, std::string_view dir);
Expand All @@ -77,6 +80,7 @@ bool isInDir(std::string_view path, std::string_view dir);
bool isDirOrInDir(std::string_view path, std::string_view dir);

/* Get status of `path'. */
struct stat stat(const Path & path);
struct stat lstat(const Path & path);

/* Return true iff the given path exists. */
Expand Down