diff --git a/src/libutil/fmt.cc b/src/libutil/fmt.cc new file mode 100644 index 000000000..914fb62b2 --- /dev/null +++ b/src/libutil/fmt.cc @@ -0,0 +1,38 @@ +#include + +namespace nix { + +std::string hiliteMatches(const std::string &s, std::vector matches, std::string prefix, std::string postfix) { + // Avoid copy on zero matches + if (matches.size() == 0) + return s; + + std::sort(matches.begin(), matches.end(), [](const auto &a, const auto &b) { + return a.position() < b.position(); + }); + + std::string out; + ssize_t last_end = 0; + + for (auto it = matches.begin(); it != matches.end();) { + auto m = *it; + size_t start = m.position(); + out.append(s.substr(last_end, m.position() - last_end)); + // Merge continous matches + ssize_t end = start + m.length(); + while(++it != matches.end() && (*it).position() <= end) { + auto n = *it; + ssize_t nend = start + (n.position() - start + n.length()); + if(nend > end) + end = nend; + } + out.append(prefix); + out.append(s.substr(start, end - start)); + out.append(postfix); + last_end = end; + } + out.append(s.substr(last_end)); + return out; +} + +} diff --git a/src/libutil/fmt.hh b/src/libutil/fmt.hh index fd335b811..1f81bfcfb 100644 --- a/src/libutil/fmt.hh +++ b/src/libutil/fmt.hh @@ -2,6 +2,7 @@ #include #include +#include #include "ansicolor.hh" @@ -154,4 +155,13 @@ inline hintformat hintfmt(std::string plain_string) // we won't be receiving any args in this case, so just print the original string return hintfmt("%s", normaltxt(plain_string)); } + +/** + * Highlight all the given matches in the given string `s` by wrapping them + * between `prefix` and `postfix`. + * + * If some matches overlap, then their union will be wrapped rather than the + * individual matches. + */ +std::string hiliteMatches(const std::string &s, std::vector matches, std::string prefix, std::string postfix); } diff --git a/src/libutil/tests/fmt.cc b/src/libutil/tests/fmt.cc new file mode 100644 index 000000000..33772162c --- /dev/null +++ b/src/libutil/tests/fmt.cc @@ -0,0 +1,68 @@ +#include "fmt.hh" + +#include + +#include + +namespace nix { +/* ----------- tests for fmt.hh -------------------------------------------------*/ + + TEST(hiliteMatches, noHighlight) { + ASSERT_STREQ(hiliteMatches("Hello, world!", std::vector(), "(", ")").c_str(), "Hello, world!"); + } + + TEST(hiliteMatches, simpleHighlight) { + std::string str = "Hello, world!"; + std::regex re = std::regex("world"); + auto matches = std::vector(std::sregex_iterator(str.begin(), str.end(), re), std::sregex_iterator()); + ASSERT_STREQ( + hiliteMatches(str, matches, "(", ")").c_str(), + "Hello, (world)!" + ); + } + + TEST(hiliteMatches, multipleMatches) { + std::string str = "Hello, world, world, world, world, world, world, Hello!"; + std::regex re = std::regex("world"); + auto matches = std::vector(std::sregex_iterator(str.begin(), str.end(), re), std::sregex_iterator()); + ASSERT_STREQ( + hiliteMatches(str, matches, "(", ")").c_str(), + "Hello, (world), (world), (world), (world), (world), (world), Hello!" + ); + } + + TEST(hiliteMatches, overlappingMatches) { + std::string str = "world, Hello, world, Hello, world, Hello, world, Hello, world!"; + std::regex re = std::regex("Hello, world"); + std::regex re2 = std::regex("world, Hello"); + auto v = std::vector(std::sregex_iterator(str.begin(), str.end(), re), std::sregex_iterator()); + for(auto it = std::sregex_iterator(str.begin(), str.end(), re2); it != std::sregex_iterator(); ++it) { + v.push_back(*it); + } + ASSERT_STREQ( + hiliteMatches(str, v, "(", ")").c_str(), + "(world, Hello, world, Hello, world, Hello, world, Hello, world)!" + ); + } + + TEST(hiliteMatches, complexOverlappingMatches) { + std::string str = "legacyPackages.x86_64-linux.git-crypt"; + std::vector regexes = { + std::regex("t-cry"), + std::regex("ux\\.git-cry"), + std::regex("git-c"), + std::regex("pt"), + }; + std::vector matches; + for(auto regex : regexes) + { + for(auto it = std::sregex_iterator(str.begin(), str.end(), regex); it != std::sregex_iterator(); ++it) { + matches.push_back(*it); + } + } + ASSERT_STREQ( + hiliteMatches(str, matches, "(", ")").c_str(), + "legacyPackages.x86_64-lin(ux.git-crypt)" + ); + } +} diff --git a/src/nix/search.cc b/src/nix/search.cc index 0d8fdd5c2..0d10d8c2e 100644 --- a/src/nix/search.cc +++ b/src/nix/search.cc @@ -9,6 +9,7 @@ #include "shared.hh" #include "eval-cache.hh" #include "attr-path.hh" +#include "fmt.hh" #include #include @@ -20,16 +21,6 @@ std::string wrap(std::string prefix, std::string s) return prefix + s + ANSI_NORMAL; } -std::string hilite(const std::string & s, const std::smatch & m, std::string postfix) -{ - return - m.empty() - ? s - : std::string(m.prefix()) - + ANSI_GREEN + std::string(m.str()) + postfix - + std::string(m.suffix()); -} - struct CmdSearch : InstallableCommand, MixJSON { std::vector res; @@ -100,8 +91,6 @@ struct CmdSearch : InstallableCommand, MixJSON }; if (cursor.isDerivation()) { - size_t found = 0; - DrvName name(cursor.getAttr("name")->getString()); auto aMeta = cursor.maybeGetAttr("meta"); @@ -110,21 +99,31 @@ struct CmdSearch : InstallableCommand, MixJSON std::replace(description.begin(), description.end(), '\n', ' '); auto attrPath2 = concatStringsSep(".", attrPath); - std::smatch attrPathMatch; - std::smatch descriptionMatch; - std::smatch nameMatch; + std::vector attrPathMatches; + std::vector descriptionMatches; + std::vector nameMatches; + bool found = false; for (auto & regex : regexes) { - std::regex_search(attrPath2, attrPathMatch, regex); - std::regex_search(name.name, nameMatch, regex); - std::regex_search(description, descriptionMatch, regex); - if (!attrPathMatch.empty() - || !nameMatch.empty() - || !descriptionMatch.empty()) - found++; + found = false; + auto add_all = [&found](std::sregex_iterator it, std::vector& vec){ + const auto end = std::sregex_iterator(); + while(it != end) { + vec.push_back(*it++); + found = true; + } + }; + + add_all(std::sregex_iterator(attrPath2.begin(), attrPath2.end(), regex), attrPathMatches); + add_all(std::sregex_iterator(name.name.begin(), name.name.end(), regex), nameMatches); + add_all(std::sregex_iterator(description.begin(), description.end(), regex), descriptionMatches); + + if(!found) + break; } - if (found == res.size()) { + if (found) + { results++; if (json) { auto jsonElem = jsonOut->object(attrPath2); @@ -132,15 +131,15 @@ struct CmdSearch : InstallableCommand, MixJSON jsonElem.attr("version", name.version); jsonElem.attr("description", description); } else { - auto name2 = hilite(name.name, nameMatch, "\e[0;2m"); + auto name2 = hiliteMatches(name.name, std::move(nameMatches), ANSI_GREEN, "\e[0;2m"); if (results > 1) logger->cout(""); logger->cout( "* %s%s", - wrap("\e[0;1m", hilite(attrPath2, attrPathMatch, "\e[0;1m")), + wrap("\e[0;1m", hiliteMatches(attrPath2, std::move(attrPathMatches), ANSI_GREEN, "\e[0;1m")), name.version != "" ? " (" + name.version + ")" : ""); if (description != "") logger->cout( - " %s", hilite(description, descriptionMatch, ANSI_NORMAL)); + " %s", hiliteMatches(description, std::move(descriptionMatches), ANSI_GREEN, ANSI_NORMAL)); } } } diff --git a/tests/search.sh b/tests/search.sh index ee3261687..52e12f381 100644 --- a/tests/search.sh +++ b/tests/search.sh @@ -23,3 +23,16 @@ clearCache nix search -f search.nix '' |grep -q foo nix search -f search.nix '' |grep -q bar nix search -f search.nix '' |grep -q hello + +## Tests for multiple regex/match highlighting + +e=$'\x1b' # grep doesn't support \e, \033 or even \x1b +# Multiple overlapping regexes +(( $(nix search -f search.nix '' 'oo' 'foo' 'oo' | grep "$e\[32;1mfoo$e\\[0;1m" | wc -l) == 1 )) +(( $(nix search -f search.nix '' 'broken b' 'en bar' | grep "$e\[32;1mbroken bar$e\\[0m" | wc -l) == 1 )) + +# Multiple matches +# Searching for 'o' should yield the 'o' in 'broken bar', the 'oo' in foo and 'o' in hello +(( $(nix search -f search.nix '' 'o' | grep -Eo "$e\[32;1mo{1,2}$e\[(0|0;1)m" | wc -l) == 3 )) +# Searching for 'b' should yield the 'b' in bar and the two 'b's in 'broken bar' +(( $(nix search -f search.nix '' 'b' | grep -Eo "$e\[32;1mb$e\[(0|0;1)m" | wc -l) == 3 ))