Merge branch 'cli-suggestions' of https://github.com/thufschmitt/nix
This commit is contained in:
commit
30ddd37873
|
@ -27,3 +27,6 @@
|
||||||
|
|
||||||
* Templates can now define a `welcomeText` attribute, which is printed out by
|
* Templates can now define a `welcomeText` attribute, which is printed out by
|
||||||
`nix flake {init,new} --template <template>`.
|
`nix flake {init,new} --template <template>`.
|
||||||
|
|
||||||
|
* Nix will now helpfully suggest some valid inputs when the user mistypes
|
||||||
|
something on the command-line
|
||||||
|
|
|
@ -272,9 +272,9 @@ void completeFlakeRefWithFragment(
|
||||||
auto attr = root->findAlongAttrPath(attrPath);
|
auto attr = root->findAlongAttrPath(attrPath);
|
||||||
if (!attr) continue;
|
if (!attr) continue;
|
||||||
|
|
||||||
for (auto & attr2 : attr->getAttrs()) {
|
for (auto & attr2 : (*attr)->getAttrs()) {
|
||||||
if (hasPrefix(attr2, lastAttr)) {
|
if (hasPrefix(attr2, lastAttr)) {
|
||||||
auto attrPath2 = attr->getAttrPath(attr2);
|
auto attrPath2 = (*attr)->getAttrPath(attr2);
|
||||||
/* Strip the attrpath prefix. */
|
/* Strip the attrpath prefix. */
|
||||||
attrPath2.erase(attrPath2.begin(), attrPath2.begin() + attrPathPrefix.size());
|
attrPath2.erase(attrPath2.begin(), attrPath2.begin() + attrPathPrefix.size());
|
||||||
completions->add(flakeRefS + "#" + concatStringsSep(".", attrPath2));
|
completions->add(flakeRefS + "#" + concatStringsSep(".", attrPath2));
|
||||||
|
@ -568,15 +568,22 @@ std::tuple<std::string, FlakeRef, InstallableValue::DerivationInfo> InstallableF
|
||||||
auto cache = openEvalCache(*state, lockedFlake);
|
auto cache = openEvalCache(*state, lockedFlake);
|
||||||
auto root = cache->getRoot();
|
auto root = cache->getRoot();
|
||||||
|
|
||||||
|
Suggestions suggestions;
|
||||||
|
|
||||||
for (auto & attrPath : getActualAttrPaths()) {
|
for (auto & attrPath : getActualAttrPaths()) {
|
||||||
debug("trying flake output attribute '%s'", attrPath);
|
debug("trying flake output attribute '%s'", attrPath);
|
||||||
|
|
||||||
auto attr = root->findAlongAttrPath(
|
auto attrOrSuggestions = root->findAlongAttrPath(
|
||||||
parseAttrPath(*state, attrPath),
|
parseAttrPath(*state, attrPath),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!attr) continue;
|
if (!attrOrSuggestions) {
|
||||||
|
suggestions += attrOrSuggestions.getSuggestions();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto attr = *attrOrSuggestions;
|
||||||
|
|
||||||
if (!attr->isDerivation())
|
if (!attr->isDerivation())
|
||||||
throw Error("flake output attribute '%s' is not a derivation", attrPath);
|
throw Error("flake output attribute '%s' is not a derivation", attrPath);
|
||||||
|
@ -591,7 +598,7 @@ std::tuple<std::string, FlakeRef, InstallableValue::DerivationInfo> InstallableF
|
||||||
return {attrPath, lockedFlake->flake.lockedRef, std::move(drvInfo)};
|
return {attrPath, lockedFlake->flake.lockedRef, std::move(drvInfo)};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw Error("flake '%s' does not provide attribute %s",
|
throw Error(suggestions, "flake '%s' does not provide attribute %s",
|
||||||
flakeRef, showAttrPaths(getActualAttrPaths()));
|
flakeRef, showAttrPaths(getActualAttrPaths()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -610,17 +617,24 @@ std::pair<Value *, Pos> InstallableFlake::toValue(EvalState & state)
|
||||||
|
|
||||||
auto emptyArgs = state.allocBindings(0);
|
auto emptyArgs = state.allocBindings(0);
|
||||||
|
|
||||||
|
Suggestions suggestions;
|
||||||
|
|
||||||
for (auto & attrPath : getActualAttrPaths()) {
|
for (auto & attrPath : getActualAttrPaths()) {
|
||||||
try {
|
try {
|
||||||
auto [v, pos] = findAlongAttrPath(state, attrPath, *emptyArgs, *vOutputs);
|
auto [v, pos] = findAlongAttrPath(state, attrPath, *emptyArgs, *vOutputs);
|
||||||
state.forceValue(*v, pos);
|
state.forceValue(*v, pos);
|
||||||
return {v, pos};
|
return {v, pos};
|
||||||
} catch (AttrPathNotFound & e) {
|
} catch (AttrPathNotFound & e) {
|
||||||
|
suggestions += e.info().suggestions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw Error("flake '%s' does not provide attribute %s",
|
throw Error(
|
||||||
flakeRef, showAttrPaths(getActualAttrPaths()));
|
suggestions,
|
||||||
|
"flake '%s' does not provide attribute %s",
|
||||||
|
flakeRef,
|
||||||
|
showAttrPaths(getActualAttrPaths())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>>
|
std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>>
|
||||||
|
@ -635,7 +649,7 @@ InstallableFlake::getCursors(EvalState & state)
|
||||||
|
|
||||||
for (auto & attrPath : getActualAttrPaths()) {
|
for (auto & attrPath : getActualAttrPaths()) {
|
||||||
auto attr = root->findAlongAttrPath(parseAttrPath(state, attrPath));
|
auto attr = root->findAlongAttrPath(parseAttrPath(state, attrPath));
|
||||||
if (attr) res.push_back({attr, attrPath});
|
if (attr) res.push_back({*attr, attrPath});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|
|
@ -74,8 +74,14 @@ std::pair<Value *, Pos> findAlongAttrPath(EvalState & state, const std::string &
|
||||||
throw Error("empty attribute name in selection path '%1%'", attrPath);
|
throw Error("empty attribute name in selection path '%1%'", attrPath);
|
||||||
|
|
||||||
Bindings::iterator a = v->attrs->find(state.symbols.create(attr));
|
Bindings::iterator a = v->attrs->find(state.symbols.create(attr));
|
||||||
if (a == v->attrs->end())
|
if (a == v->attrs->end()) {
|
||||||
throw AttrPathNotFound("attribute '%1%' in selection path '%2%' not found", attr, attrPath);
|
std::set<std::string> attrNames;
|
||||||
|
for (auto & attr : *v->attrs)
|
||||||
|
attrNames.insert(attr.name);
|
||||||
|
|
||||||
|
auto suggestions = Suggestions::bestMatches(attrNames, attr);
|
||||||
|
throw AttrPathNotFound(suggestions, "attribute '%1%' in selection path '%2%' not found", attr, attrPath);
|
||||||
|
}
|
||||||
v = &*a->value;
|
v = &*a->value;
|
||||||
pos = *a->pos;
|
pos = *a->pos;
|
||||||
}
|
}
|
||||||
|
|
|
@ -406,6 +406,16 @@ Value & AttrCursor::forceValue()
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Suggestions AttrCursor::getSuggestionsForAttr(Symbol name)
|
||||||
|
{
|
||||||
|
auto attrNames = getAttrs();
|
||||||
|
std::set<std::string> strAttrNames;
|
||||||
|
for (auto & name : attrNames)
|
||||||
|
strAttrNames.insert(std::string(name));
|
||||||
|
|
||||||
|
return Suggestions::bestMatches(strAttrNames, name);
|
||||||
|
}
|
||||||
|
|
||||||
std::shared_ptr<AttrCursor> AttrCursor::maybeGetAttr(Symbol name, bool forceErrors)
|
std::shared_ptr<AttrCursor> AttrCursor::maybeGetAttr(Symbol name, bool forceErrors)
|
||||||
{
|
{
|
||||||
if (root->db) {
|
if (root->db) {
|
||||||
|
@ -446,6 +456,11 @@ std::shared_ptr<AttrCursor> AttrCursor::maybeGetAttr(Symbol name, bool forceErro
|
||||||
return nullptr;
|
return nullptr;
|
||||||
//throw TypeError("'%s' is not an attribute set", getAttrPathStr());
|
//throw TypeError("'%s' is not an attribute set", getAttrPathStr());
|
||||||
|
|
||||||
|
for (auto & attr : *v.attrs) {
|
||||||
|
if (root->db)
|
||||||
|
root->db->setPlaceholder({cachedValue->first, attr.name});
|
||||||
|
}
|
||||||
|
|
||||||
auto attr = v.attrs->get(name);
|
auto attr = v.attrs->get(name);
|
||||||
|
|
||||||
if (!attr) {
|
if (!attr) {
|
||||||
|
@ -464,7 +479,7 @@ std::shared_ptr<AttrCursor> AttrCursor::maybeGetAttr(Symbol name, bool forceErro
|
||||||
cachedValue2 = {root->db->setPlaceholder({cachedValue->first, name}), placeholder_t()};
|
cachedValue2 = {root->db->setPlaceholder({cachedValue->first, name}), placeholder_t()};
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::make_shared<AttrCursor>(
|
return make_ref<AttrCursor>(
|
||||||
root, std::make_pair(shared_from_this(), name), attr->value, std::move(cachedValue2));
|
root, std::make_pair(shared_from_this(), name), attr->value, std::move(cachedValue2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -473,27 +488,31 @@ std::shared_ptr<AttrCursor> AttrCursor::maybeGetAttr(std::string_view name)
|
||||||
return maybeGetAttr(root->state.symbols.create(name));
|
return maybeGetAttr(root->state.symbols.create(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<AttrCursor> AttrCursor::getAttr(Symbol name, bool forceErrors)
|
ref<AttrCursor> AttrCursor::getAttr(Symbol name, bool forceErrors)
|
||||||
{
|
{
|
||||||
auto p = maybeGetAttr(name, forceErrors);
|
auto p = maybeGetAttr(name, forceErrors);
|
||||||
if (!p)
|
if (!p)
|
||||||
throw Error("attribute '%s' does not exist", getAttrPathStr(name));
|
throw Error("attribute '%s' does not exist", getAttrPathStr(name));
|
||||||
return p;
|
return ref(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<AttrCursor> AttrCursor::getAttr(std::string_view name)
|
ref<AttrCursor> AttrCursor::getAttr(std::string_view name)
|
||||||
{
|
{
|
||||||
return getAttr(root->state.symbols.create(name));
|
return getAttr(root->state.symbols.create(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<AttrCursor> AttrCursor::findAlongAttrPath(const std::vector<Symbol> & attrPath, bool force)
|
OrSuggestions<ref<AttrCursor>> AttrCursor::findAlongAttrPath(const std::vector<Symbol> & attrPath, bool force)
|
||||||
{
|
{
|
||||||
auto res = shared_from_this();
|
auto res = shared_from_this();
|
||||||
for (auto & attr : attrPath) {
|
for (auto & attr : attrPath) {
|
||||||
res = res->maybeGetAttr(attr, force);
|
auto child = res->maybeGetAttr(attr, force);
|
||||||
if (!res) return {};
|
if (!child) {
|
||||||
|
auto suggestions = res->getSuggestionsForAttr(attr);
|
||||||
|
return OrSuggestions<ref<AttrCursor>>::failed(suggestions);
|
||||||
|
}
|
||||||
|
res = child;
|
||||||
}
|
}
|
||||||
return res;
|
return ref(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string AttrCursor::getString()
|
std::string AttrCursor::getString()
|
||||||
|
|
|
@ -94,15 +94,17 @@ public:
|
||||||
|
|
||||||
std::string getAttrPathStr(Symbol name) const;
|
std::string getAttrPathStr(Symbol name) const;
|
||||||
|
|
||||||
|
Suggestions getSuggestionsForAttr(Symbol name);
|
||||||
|
|
||||||
std::shared_ptr<AttrCursor> maybeGetAttr(Symbol name, bool forceErrors = false);
|
std::shared_ptr<AttrCursor> maybeGetAttr(Symbol name, bool forceErrors = false);
|
||||||
|
|
||||||
std::shared_ptr<AttrCursor> maybeGetAttr(std::string_view name);
|
std::shared_ptr<AttrCursor> maybeGetAttr(std::string_view name);
|
||||||
|
|
||||||
std::shared_ptr<AttrCursor> getAttr(Symbol name, bool forceErrors = false);
|
ref<AttrCursor> getAttr(Symbol name, bool forceErrors = false);
|
||||||
|
|
||||||
std::shared_ptr<AttrCursor> getAttr(std::string_view name);
|
ref<AttrCursor> getAttr(std::string_view name);
|
||||||
|
|
||||||
std::shared_ptr<AttrCursor> findAlongAttrPath(const std::vector<Symbol> & attrPath, bool force = false);
|
OrSuggestions<ref<AttrCursor>> findAlongAttrPath(const std::vector<Symbol> & attrPath, bool force = false);
|
||||||
|
|
||||||
std::string getString();
|
std::string getString();
|
||||||
|
|
||||||
|
|
|
@ -328,8 +328,13 @@ MultiCommand::MultiCommand(const Commands & commands_)
|
||||||
completions->add(name);
|
completions->add(name);
|
||||||
}
|
}
|
||||||
auto i = commands.find(s);
|
auto i = commands.find(s);
|
||||||
if (i == commands.end())
|
if (i == commands.end()) {
|
||||||
throw UsageError("'%s' is not a recognised command", s);
|
std::set<std::string> commandNames;
|
||||||
|
for (auto & [name, _] : commands)
|
||||||
|
commandNames.insert(name);
|
||||||
|
auto suggestions = Suggestions::bestMatches(commandNames, s);
|
||||||
|
throw UsageError(suggestions, "'%s' is not a recognised command", s);
|
||||||
|
}
|
||||||
command = {s, i->second()};
|
command = {s, i->second()};
|
||||||
command->second->parent = this;
|
command->second->parent = this;
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -282,6 +282,13 @@ std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto suggestions = einfo.suggestions.trim();
|
||||||
|
if (! suggestions.suggestions.empty()){
|
||||||
|
oss << "Did you mean " <<
|
||||||
|
suggestions.trim() <<
|
||||||
|
"?" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
// traces
|
// traces
|
||||||
if (showTrace && !einfo.traces.empty()) {
|
if (showTrace && !einfo.traces.empty()) {
|
||||||
for (auto iter = einfo.traces.rbegin(); iter != einfo.traces.rend(); ++iter) {
|
for (auto iter = einfo.traces.rbegin(); iter != einfo.traces.rend(); ++iter) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "suggestions.hh"
|
||||||
#include "ref.hh"
|
#include "ref.hh"
|
||||||
#include "types.hh"
|
#include "types.hh"
|
||||||
#include "fmt.hh"
|
#include "fmt.hh"
|
||||||
|
@ -112,6 +113,8 @@ struct ErrorInfo {
|
||||||
std::optional<ErrPos> errPos;
|
std::optional<ErrPos> errPos;
|
||||||
std::list<Trace> traces;
|
std::list<Trace> traces;
|
||||||
|
|
||||||
|
Suggestions suggestions;
|
||||||
|
|
||||||
static std::optional<std::string> programName;
|
static std::optional<std::string> programName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -141,6 +144,11 @@ public:
|
||||||
: err { .level = lvlError, .msg = hintfmt(fs, args...) }
|
: err { .level = lvlError, .msg = hintfmt(fs, args...) }
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
template<typename... Args>
|
||||||
|
BaseError(const Suggestions & sug, const Args & ... args)
|
||||||
|
: err { .level = lvlError, .msg = hintfmt(args...), .suggestions = sug }
|
||||||
|
{ }
|
||||||
|
|
||||||
BaseError(hintformat hint)
|
BaseError(hintformat hint)
|
||||||
: err { .level = lvlError, .msg = hint }
|
: err { .level = lvlError, .msg = hint }
|
||||||
{ }
|
{ }
|
||||||
|
|
114
src/libutil/suggestions.cc
Normal file
114
src/libutil/suggestions.cc
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
#include "suggestions.hh"
|
||||||
|
#include "ansicolor.hh"
|
||||||
|
#include "util.hh"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
|
||||||
|
int levenshteinDistance(std::string_view first, std::string_view second)
|
||||||
|
{
|
||||||
|
// Implementation borrowed from
|
||||||
|
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
||||||
|
|
||||||
|
int m = first.size();
|
||||||
|
int n = second.size();
|
||||||
|
|
||||||
|
auto v0 = std::vector<int>(n+1);
|
||||||
|
auto v1 = std::vector<int>(n+1);
|
||||||
|
|
||||||
|
for (auto i = 0; i <= n; i++)
|
||||||
|
v0[i] = i;
|
||||||
|
|
||||||
|
for (auto i = 0; i < m; i++) {
|
||||||
|
v1[0] = i+1;
|
||||||
|
|
||||||
|
for (auto j = 0; j < n; j++) {
|
||||||
|
auto deletionCost = v0[j+1] + 1;
|
||||||
|
auto insertionCost = v1[j] + 1;
|
||||||
|
auto substitutionCost = first[i] == second[j] ? v0[j] : v0[j] + 1;
|
||||||
|
v1[j+1] = std::min({deletionCost, insertionCost, substitutionCost});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::swap(v0, v1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return v0[n];
|
||||||
|
}
|
||||||
|
|
||||||
|
Suggestions Suggestions::bestMatches (
|
||||||
|
std::set<std::string> allMatches,
|
||||||
|
std::string query)
|
||||||
|
{
|
||||||
|
std::set<Suggestion> res;
|
||||||
|
for (const auto & possibleMatch : allMatches) {
|
||||||
|
res.insert(Suggestion {
|
||||||
|
.distance = levenshteinDistance(query, possibleMatch),
|
||||||
|
.suggestion = possibleMatch,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Suggestions { res };
|
||||||
|
}
|
||||||
|
|
||||||
|
Suggestions Suggestions::trim(int limit, int maxDistance) const
|
||||||
|
{
|
||||||
|
std::set<Suggestion> res;
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (auto & elt : suggestions) {
|
||||||
|
if (count >= limit || elt.distance > maxDistance)
|
||||||
|
break;
|
||||||
|
count++;
|
||||||
|
res.insert(elt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Suggestions{res};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Suggestion::to_string() const
|
||||||
|
{
|
||||||
|
return ANSI_WARNING + filterANSIEscapes(suggestion) + ANSI_NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Suggestions::to_string() const
|
||||||
|
{
|
||||||
|
switch (suggestions.size()) {
|
||||||
|
case 0:
|
||||||
|
return "";
|
||||||
|
case 1:
|
||||||
|
return suggestions.begin()->to_string();
|
||||||
|
default: {
|
||||||
|
std::string res = "one of ";
|
||||||
|
auto iter = suggestions.begin();
|
||||||
|
res += iter->to_string(); // Iter can’t be end() because the container isn’t null
|
||||||
|
iter++;
|
||||||
|
auto last = suggestions.end(); last--;
|
||||||
|
for ( ; iter != suggestions.end() ; iter++) {
|
||||||
|
res += (iter == last) ? " or " : ", ";
|
||||||
|
res += iter->to_string();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Suggestions & Suggestions::operator+=(const Suggestions & other)
|
||||||
|
{
|
||||||
|
suggestions.insert(
|
||||||
|
other.suggestions.begin(),
|
||||||
|
other.suggestions.end()
|
||||||
|
);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream & operator<<(std::ostream & str, const Suggestion & suggestion)
|
||||||
|
{
|
||||||
|
return str << suggestion.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream & operator<<(std::ostream & str, const Suggestions & suggestions)
|
||||||
|
{
|
||||||
|
return str << suggestions.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
102
src/libutil/suggestions.hh
Normal file
102
src/libutil/suggestions.hh
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "comparator.hh"
|
||||||
|
#include "types.hh"
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
|
||||||
|
int levenshteinDistance(std::string_view first, std::string_view second);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A potential suggestion for the cli interface.
|
||||||
|
*/
|
||||||
|
class Suggestion {
|
||||||
|
public:
|
||||||
|
int distance; // The smaller the better
|
||||||
|
std::string suggestion;
|
||||||
|
|
||||||
|
std::string to_string() const;
|
||||||
|
|
||||||
|
GENERATE_CMP(Suggestion, me->distance, me->suggestion)
|
||||||
|
};
|
||||||
|
|
||||||
|
class Suggestions {
|
||||||
|
public:
|
||||||
|
std::set<Suggestion> suggestions;
|
||||||
|
|
||||||
|
std::string to_string() const;
|
||||||
|
|
||||||
|
Suggestions trim(
|
||||||
|
int limit = 5,
|
||||||
|
int maxDistance = 2
|
||||||
|
) const;
|
||||||
|
|
||||||
|
static Suggestions bestMatches (
|
||||||
|
std::set<std::string> allMatches,
|
||||||
|
std::string query
|
||||||
|
);
|
||||||
|
|
||||||
|
Suggestions& operator+=(const Suggestions & other);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::ostream & operator<<(std::ostream & str, const Suggestion &);
|
||||||
|
std::ostream & operator<<(std::ostream & str, const Suggestions &);
|
||||||
|
|
||||||
|
// Either a value of type `T`, or some suggestions
|
||||||
|
template<typename T>
|
||||||
|
class OrSuggestions {
|
||||||
|
public:
|
||||||
|
using Raw = std::variant<T, Suggestions>;
|
||||||
|
|
||||||
|
Raw raw;
|
||||||
|
|
||||||
|
T* operator ->()
|
||||||
|
{
|
||||||
|
return &**this;
|
||||||
|
}
|
||||||
|
|
||||||
|
T& operator *()
|
||||||
|
{
|
||||||
|
return std::get<T>(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
operator bool() const noexcept
|
||||||
|
{
|
||||||
|
return std::holds_alternative<T>(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
OrSuggestions(T t)
|
||||||
|
: raw(t)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
OrSuggestions()
|
||||||
|
: raw(Suggestions{})
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
static OrSuggestions<T> failed(const Suggestions & s)
|
||||||
|
{
|
||||||
|
auto res = OrSuggestions<T>();
|
||||||
|
res.raw = s;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static OrSuggestions<T> failed()
|
||||||
|
{
|
||||||
|
return OrSuggestions<T>::failed(Suggestions{});
|
||||||
|
}
|
||||||
|
|
||||||
|
const Suggestions & getSuggestions()
|
||||||
|
{
|
||||||
|
static Suggestions noSuggestions;
|
||||||
|
if (const auto & suggestions = std::get_if<Suggestions>(&raw))
|
||||||
|
return *suggestions;
|
||||||
|
else
|
||||||
|
return noSuggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
43
src/libutil/tests/suggestions.cc
Normal file
43
src/libutil/tests/suggestions.cc
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#include "suggestions.hh"
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
|
||||||
|
struct LevenshteinDistanceParam {
|
||||||
|
std::string s1, s2;
|
||||||
|
int distance;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LevenshteinDistanceTest :
|
||||||
|
public testing::TestWithParam<LevenshteinDistanceParam> {
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_P(LevenshteinDistanceTest, CorrectlyComputed) {
|
||||||
|
auto params = GetParam();
|
||||||
|
|
||||||
|
ASSERT_EQ(levenshteinDistance(params.s1, params.s2), params.distance);
|
||||||
|
ASSERT_EQ(levenshteinDistance(params.s2, params.s1), params.distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTANTIATE_TEST_SUITE_P(LevenshteinDistance, LevenshteinDistanceTest,
|
||||||
|
testing::Values(
|
||||||
|
LevenshteinDistanceParam{"foo", "foo", 0},
|
||||||
|
LevenshteinDistanceParam{"foo", "", 3},
|
||||||
|
LevenshteinDistanceParam{"", "", 0},
|
||||||
|
LevenshteinDistanceParam{"foo", "fo", 1},
|
||||||
|
LevenshteinDistanceParam{"foo", "oo", 1},
|
||||||
|
LevenshteinDistanceParam{"foo", "fao", 1},
|
||||||
|
LevenshteinDistanceParam{"foo", "abc", 3}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
TEST(Suggestions, Trim) {
|
||||||
|
auto suggestions = Suggestions::bestMatches({"foooo", "bar", "fo", "gao"}, "foo");
|
||||||
|
auto onlyOne = suggestions.trim(1);
|
||||||
|
ASSERT_EQ(onlyOne.suggestions.size(), 1);
|
||||||
|
ASSERT_TRUE(onlyOne.suggestions.begin()->suggestion == "fo");
|
||||||
|
|
||||||
|
auto closest = suggestions.trim(999, 2);
|
||||||
|
ASSERT_EQ(closest.suggestions.size(), 3);
|
||||||
|
}
|
||||||
|
}
|
|
@ -92,8 +92,9 @@ nix_tests = \
|
||||||
bash-profile.sh \
|
bash-profile.sh \
|
||||||
pass-as-file.sh \
|
pass-as-file.sh \
|
||||||
describe-stores.sh \
|
describe-stores.sh \
|
||||||
store-ping.sh \
|
nix-profile.sh \
|
||||||
nix-profile.sh
|
suggestions.sh \
|
||||||
|
store-ping.sh
|
||||||
|
|
||||||
ifeq ($(HAVE_LIBCPUID), 1)
|
ifeq ($(HAVE_LIBCPUID), 1)
|
||||||
nix_tests += compute-levels.sh
|
nix_tests += compute-levels.sh
|
||||||
|
|
36
tests/suggestions.sh
Normal file
36
tests/suggestions.sh
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
source common.sh
|
||||||
|
|
||||||
|
clearStore
|
||||||
|
|
||||||
|
cd "$TEST_HOME"
|
||||||
|
|
||||||
|
cat <<EOF > flake.nix
|
||||||
|
{
|
||||||
|
outputs = a: {
|
||||||
|
packages.$system = {
|
||||||
|
foo = 1;
|
||||||
|
fo1 = 1;
|
||||||
|
fo2 = 1;
|
||||||
|
fooo = 1;
|
||||||
|
foooo = 1;
|
||||||
|
fooooo = 1;
|
||||||
|
fooooo1 = 1;
|
||||||
|
fooooo2 = 1;
|
||||||
|
fooooo3 = 1;
|
||||||
|
fooooo4 = 1;
|
||||||
|
fooooo5 = 1;
|
||||||
|
fooooo6 = 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Probable typo in the requested attribute path. Suggest some close possibilities
|
||||||
|
NIX_BUILD_STDERR_WITH_SUGGESTIONS=$(! nix build .\#fob 2>&1 1>/dev/null)
|
||||||
|
[[ "$NIX_BUILD_STDERR_WITH_SUGGESTIONS" =~ "Did you mean one of fo1, fo2, foo or fooo?" ]] || \
|
||||||
|
fail "The nix build stderr should suggest the three closest possiblities"
|
||||||
|
|
||||||
|
# None of the possible attributes is close to `bar`, so shouldn’t suggest anything
|
||||||
|
NIX_BUILD_STDERR_WITH_NO_CLOSE_SUGGESTION=$(! nix build .\#bar 2>&1 1>/dev/null)
|
||||||
|
[[ ! "$NIX_BUILD_STDERR_WITH_NO_CLOSE_SUGGESTION" =~ "Did you mean" ]] || \
|
||||||
|
fail "The nix build stderr shouldn’t suggest anything if there’s nothing relevant to suggest"
|
Loading…
Reference in a new issue