From 06245f37131abd40ff8291f4b556fac7895d1036 Mon Sep 17 00:00:00 2001 From: LosFarmosCTL <80157503+LosFarmosCTL@users.noreply.github.com> Date: Sun, 17 Oct 2021 14:36:44 +0200 Subject: [PATCH] Add new search predicate to enable searching for messages matching a regex (#3282) Co-authored-by: pajlada --- CHANGELOG.md | 1 + chatterino.pro | 1 + src/CMakeLists.txt | 2 + src/messages/search/RegexPredicate.cpp | 22 +++++++ src/messages/search/RegexPredicate.hpp | 42 ++++++++++++++ src/widgets/helper/SearchPopup.cpp | 80 +++++++++++++------------- 6 files changed, 109 insertions(+), 39 deletions(-) create mode 100644 src/messages/search/RegexPredicate.cpp create mode 100644 src/messages/search/RegexPredicate.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d6fbb4b..9f214285b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unversioned +- Minor: Added new search predicate to filter for messages matching a regex (#3282) - Minor: Add `{channel.name}`, `{channel.id}`, `{stream.game}`, `{stream.title}`, `{my.id}`, `{my.name}` placeholders for commands (#3155) - Minor: Remove TwitchEmotes.com attribution and the open/copy options when right-clicking a Twitch Emote. (#2214, #3136) - Minor: Strip leading @ and trailing , from username in /user and /usercard commands. (#3143) diff --git a/chatterino.pro b/chatterino.pro index 33fdfedee..fb850cd13 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -185,6 +185,7 @@ SOURCES += \ src/messages/search/ChannelPredicate.cpp \ src/messages/search/LinkPredicate.cpp \ src/messages/search/MessageFlagsPredicate.cpp \ + src/messages/search/RegexPredicate.cpp \ src/messages/search/SubstringPredicate.cpp \ src/messages/SharedMessageBuilder.cpp \ src/providers/bttv/BttvEmotes.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e3ac9790b..e78de47b8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -149,6 +149,8 @@ set(SOURCE_FILES messages/search/LinkPredicate.hpp messages/search/MessageFlagsPredicate.cpp messages/search/MessageFlagsPredicate.hpp + messages/search/RegexPredicate.cpp + messages/search/RegexPredicate.hpp messages/search/SubstringPredicate.cpp messages/search/SubstringPredicate.hpp diff --git a/src/messages/search/RegexPredicate.cpp b/src/messages/search/RegexPredicate.cpp new file mode 100644 index 000000000..b16dcf4d1 --- /dev/null +++ b/src/messages/search/RegexPredicate.cpp @@ -0,0 +1,22 @@ +#include "RegexPredicate.hpp" + +namespace chatterino { + +RegexPredicate::RegexPredicate(const QString ®ex) + : regex_(regex, QRegularExpression::CaseInsensitiveOption) +{ +} + +bool RegexPredicate::appliesTo(const Message &message) +{ + if (!regex_.isValid()) + { + return false; + } + + QRegularExpressionMatch match = regex_.match(message.messageText); + + return match.hasMatch(); +} + +} // namespace chatterino \ No newline at end of file diff --git a/src/messages/search/RegexPredicate.hpp b/src/messages/search/RegexPredicate.hpp new file mode 100644 index 000000000..bacf1e6dc --- /dev/null +++ b/src/messages/search/RegexPredicate.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include "QRegularExpression" +#include "messages/search/MessagePredicate.hpp" + +namespace chatterino { + +/** + * @brief MessagePredicate checking whether the message matches a given regex. + * + * This predicate will only allow messages whose `messageText` match the given + * regex. + */ +class RegexPredicate : public MessagePredicate +{ +public: + /** + * @brief Create a RegexPredicate with a regex to match the message against. + * + * The message is being matched case-insensitively. + * + * @param regex the regex to match the message against + */ + RegexPredicate(const QString ®ex); + + /** + * @brief Checks whether the message matches the regex passed in the + * constructor + * + * The check is done case-insensitively. + * + * @param message the message to check + * @return true if the message matches the regex, false otherwise + */ + bool appliesTo(const Message &message); + +private: + /// Holds the regular expression to match the message against + QRegularExpression regex_; +}; + +} // namespace chatterino \ No newline at end of file diff --git a/src/widgets/helper/SearchPopup.cpp b/src/widgets/helper/SearchPopup.cpp index 0f87bcf27..d96e0fa6c 100644 --- a/src/widgets/helper/SearchPopup.cpp +++ b/src/widgets/helper/SearchPopup.cpp @@ -11,6 +11,7 @@ #include "messages/search/ChannelPredicate.hpp" #include "messages/search/LinkPredicate.hpp" #include "messages/search/MessageFlagsPredicate.hpp" +#include "messages/search/RegexPredicate.hpp" #include "messages/search/SubstringPredicate.hpp" #include "util/Shortcut.hpp" #include "widgets/helper/ChannelView.hpp" @@ -164,51 +165,56 @@ void SearchPopup::initLayout() std::vector> SearchPopup::parsePredicates( const QString &input) { - static QRegularExpression predicateRegex(R"(^(\w+):([\w,]+)$)"); + // This regex captures all name:value predicate pairs into named capturing + // groups and matches all other inputs seperated by spaces as normal + // strings. + // It also ignores whitespaces in values when being surrounded by quotation + // marks, to enable inputs like this => regex:"kappa 123" + static QRegularExpression predicateRegex( + R"lit((?:(?\w+):(?".+?"|[^\s]+))|[^\s]+?(?=$|\s))lit"); + static QRegularExpression trimQuotationMarksRegex(R"(^"|"$)"); + + QRegularExpressionMatchIterator it = predicateRegex.globalMatch(input); std::vector> predicates; - auto words = input.split(' ', QString::SkipEmptyParts); QStringList authors; QStringList channels; - for (auto it = words.begin(); it != words.end();) + while (it.hasNext()) { - if (auto match = predicateRegex.match(*it); match.hasMatch()) + QRegularExpressionMatch match = it.next(); + + QString name = match.captured("name"); + + QString value = match.captured("value"); + value.remove(trimQuotationMarksRegex); + + // match predicates + if (name == "from") { - QString name = match.captured(1); - QString value = match.captured(2); - - bool remove = true; - - // match predicates - if (name == "from") - { - authors.append(value); - } - else if (name == "has" && value == "link") - { - predicates.push_back(std::make_unique()); - } - else if (name == "in") - { - channels.append(value); - } - else if (name == "is") - { - predicates.push_back( - std::make_unique(value)); - } - else - { - remove = false; - } - - // remove or advance - it = remove ? words.erase(it) : ++it; + authors.append(value); + } + else if (name == "has" && value == "link") + { + predicates.push_back(std::make_unique()); + } + else if (name == "in") + { + channels.append(value); + } + else if (name == "is") + { + predicates.push_back( + std::make_unique(value)); + } + else if (name == "regex") + { + predicates.push_back(std::make_unique(value)); } else { - ++it; + predicates.push_back( + std::make_unique(match.captured())); } } @@ -218,10 +224,6 @@ std::vector> SearchPopup::parsePredicates( if (!channels.empty()) predicates.push_back(std::make_unique(channels)); - if (!words.empty()) - predicates.push_back( - std::make_unique(words.join(" "))); - return predicates; }