mirror of
https://github.com/NixOS/nix.git
synced 2025-11-12 05:26:02 +01:00
Merge branch 'cli-suggestions' of https://github.com/thufschmitt/nix
This commit is contained in:
commit
30ddd37873
13 changed files with 385 additions and 25 deletions
|
|
@ -328,8 +328,13 @@ MultiCommand::MultiCommand(const Commands & commands_)
|
|||
completions->add(name);
|
||||
}
|
||||
auto i = commands.find(s);
|
||||
if (i == commands.end())
|
||||
throw UsageError("'%s' is not a recognised command", s);
|
||||
if (i == commands.end()) {
|
||||
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->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
|
||||
if (showTrace && !einfo.traces.empty()) {
|
||||
for (auto iter = einfo.traces.rbegin(); iter != einfo.traces.rend(); ++iter) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include "suggestions.hh"
|
||||
#include "ref.hh"
|
||||
#include "types.hh"
|
||||
#include "fmt.hh"
|
||||
|
|
@ -112,6 +113,8 @@ struct ErrorInfo {
|
|||
std::optional<ErrPos> errPos;
|
||||
std::list<Trace> traces;
|
||||
|
||||
Suggestions suggestions;
|
||||
|
||||
static std::optional<std::string> programName;
|
||||
};
|
||||
|
||||
|
|
@ -141,6 +144,11 @@ public:
|
|||
: 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)
|
||||
: 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue