feat: Allow negation of search predicates (#4207)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
closes https://github.com/Chatterino/chatterino2/issues/3998
This commit is contained in:
kornes 2022-12-04 11:34:13 +00:00 committed by GitHub
parent 4fa214a38a
commit b7888749fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 139 additions and 111 deletions

View file

@ -2,6 +2,7 @@
## Unversioned
- Minor: Added ability to negate search options by prefixing it with an exclamation mark (e.g. `!badge:mod` to search for messages where the author does not have the moderator badge). (#4207)
- Minor: Search window input will automatically use currently selected text if present. (#4178)
- Minor: Cleared up highlight sound settings (#4194)
- Bugfix: Fixed highlight sounds not reloading on change properly. (#4194)

View file

@ -4,20 +4,18 @@
namespace chatterino {
AuthorPredicate::AuthorPredicate(const QStringList &authors)
: authors_()
AuthorPredicate::AuthorPredicate(const QString &authors, bool negate)
: MessagePredicate(negate)
, authors_()
{
// Check if any comma-seperated values were passed and transform those
for (const auto &entry : authors)
{
for (const auto &author : entry.split(',', Qt::SkipEmptyParts))
for (const auto &author : authors.split(',', Qt::SkipEmptyParts))
{
this->authors_ << author;
}
}
}
bool AuthorPredicate::appliesTo(const Message &message)
bool AuthorPredicate::appliesToImpl(const Message &message)
{
return authors_.contains(message.displayName, Qt::CaseInsensitive) ||
authors_.contains(message.loginName, Qt::CaseInsensitive);

View file

@ -16,10 +16,12 @@ public:
/**
* @brief Create an AuthorPredicate with a list of users to search for.
*
* @param authors a list of user names that a message should be sent from
* @param authors one or more comma-separated user names that a message should be sent from
* @param negate when set, excludes list of user names from results
*/
AuthorPredicate(const QStringList &authors);
AuthorPredicate(const QString &authors, bool negate);
protected:
/**
* @brief Checks whether the message is authored by any of the users passed
* in the constructor.
@ -28,7 +30,7 @@ public:
* @return true if the message was authored by one of the specified users,
* false otherwise
*/
bool appliesTo(const Message &message);
bool appliesToImpl(const Message &message) override;
private:
/// Holds the user names that will be searched for

View file

@ -4,12 +4,11 @@
namespace chatterino {
BadgePredicate::BadgePredicate(const QStringList &badges)
BadgePredicate::BadgePredicate(const QString &badges, bool negate)
: MessagePredicate(negate)
{
// Check if any comma-seperated values were passed and transform those
for (const auto &entry : badges)
{
for (const auto &badge : entry.split(',', Qt::SkipEmptyParts))
for (const auto &badge : badges.split(',', Qt::SkipEmptyParts))
{
// convert short form name of certain badges to formal name
if (badge.compare("mod", Qt::CaseInsensitive) == 0)
@ -30,9 +29,8 @@ BadgePredicate::BadgePredicate(const QStringList &badges)
}
}
}
}
bool BadgePredicate::appliesTo(const Message &message)
bool BadgePredicate::appliesToImpl(const Message &message)
{
for (const Badge &badge : message.badges)
{

View file

@ -16,10 +16,12 @@ public:
/**
* @brief Create an BadgePredicate with a list of badges to search for.
*
* @param badges a list of badges that a message should contain
* @param badges one or more comma-separated badges that a message should contain
* @param negate when set, excludes list of badges from results
*/
BadgePredicate(const QStringList &badges);
BadgePredicate(const QString &badges, bool negate);
protected:
/**
* @brief Checks whether the message contains any of the badges passed
* in the constructor.
@ -28,7 +30,7 @@ public:
* @return true if the message contains a badge listed in the specified badges,
* false otherwise
*/
bool appliesTo(const Message &message) override;
bool appliesToImpl(const Message &message) override;
private:
/// Holds the badges that will be searched for

View file

@ -4,20 +4,18 @@
namespace chatterino {
ChannelPredicate::ChannelPredicate(const QStringList &channels)
: channels_()
ChannelPredicate::ChannelPredicate(const QString &channels, bool negate)
: MessagePredicate(negate)
, channels_()
{
// Check if any comma-seperated values were passed and transform those
for (const auto &entry : channels)
{
for (const auto &channel : entry.split(',', Qt::SkipEmptyParts))
for (const auto &channel : channels.split(',', Qt::SkipEmptyParts))
{
this->channels_ << channel;
}
}
}
bool ChannelPredicate::appliesTo(const Message &message)
bool ChannelPredicate::appliesToImpl(const Message &message)
{
return channels_.contains(message.channelName, Qt::CaseInsensitive);
}

View file

@ -16,10 +16,12 @@ public:
/**
* @brief Create a ChannelPredicate with a list of channels to search for.
*
* @param channels a list of channel names that a message should be sent in
* @param channels one or more comma-separated channel names that a message should be sent in
* @param negate when set, excludes list of channel names from results
*/
ChannelPredicate(const QStringList &channels);
ChannelPredicate(const QString &channels, bool negate);
protected:
/**
* @brief Checks whether the message was sent in any of the channels passed
* in the constructor.
@ -28,7 +30,7 @@ public:
* @return true if the message was sent in one of the specified channels,
* false otherwise
*/
bool appliesTo(const Message &message);
bool appliesToImpl(const Message &message) override;
private:
/// Holds the channel names that will be searched for

View file

@ -5,11 +5,12 @@
namespace chatterino {
LinkPredicate::LinkPredicate()
LinkPredicate::LinkPredicate(bool negate)
: MessagePredicate(negate)
{
}
bool LinkPredicate::appliesTo(const Message &message)
bool LinkPredicate::appliesToImpl(const Message &message)
{
for (const auto &word : message.messageText.split(' ', Qt::SkipEmptyParts))
{

View file

@ -12,15 +12,21 @@ namespace chatterino {
class LinkPredicate : public MessagePredicate
{
public:
LinkPredicate();
/**
* @brief Create an LinkPredicate
*
* @param negate when set, excludes messages containing links from results
*/
LinkPredicate(bool negate);
protected:
/**
* @brief Checks whether the message contains a link.
*
* @param message the message to check
* @return true if the message contains a link, false otherwise
*/
bool appliesTo(const Message &message);
bool appliesToImpl(const Message &message) override;
};
} // namespace chatterino

View file

@ -4,8 +4,9 @@
namespace chatterino {
MessageFlagsPredicate::MessageFlagsPredicate(const QString &flags)
: flags_()
MessageFlagsPredicate::MessageFlagsPredicate(const QString &flags, bool negate)
: MessagePredicate(negate)
, flags_()
{
// Check if any comma-seperated values were passed and transform those
for (const auto &flag : flags.split(',', Qt::SkipEmptyParts))
@ -54,13 +55,15 @@ MessageFlagsPredicate::MessageFlagsPredicate(const QString &flags)
}
}
bool MessageFlagsPredicate::appliesTo(const Message &message)
bool MessageFlagsPredicate::appliesToImpl(const Message &message)
{
// Exclude timeout messages from system flag when timeout flag isn't present
if (this->flags_.has(MessageFlag::System) &&
!this->flags_.has(MessageFlag::Timeout))
{
return message.flags.hasAny(flags_) &&
!message.flags.has(MessageFlag::Timeout);
}
return message.flags.hasAny(flags_);
}

View file

@ -27,9 +27,11 @@ public:
* "system" is used for the "System" flag.
*
* @param flags a string comma seperated list of names for the flags a message should have
* @param negate when set, excludes messages containg selected flags from results
*/
MessageFlagsPredicate(const QString &flags);
MessageFlagsPredicate(const QString &flags, bool negate);
protected:
/**
* @brief Checks whether the message has any of the flags passed
* in the constructor.
@ -38,7 +40,7 @@ public:
* @return true if the message has at least one of the specified flags,
* false otherwise
*/
bool appliesTo(const Message &message);
bool appliesToImpl(const Message &message) override;
private:
/// Holds the flags that will be searched for

View file

@ -11,7 +11,7 @@ namespace chatterino {
*
* Message predicates define certain features a message can satisfy.
* Features are represented by classes derived from this abstract class.
* A derived class must override `appliesTo` in order to test for the desired
* A derived class must override `appliesToImpl` in order to test for the desired
* feature.
*/
class MessagePredicate
@ -19,15 +19,43 @@ class MessagePredicate
public:
virtual ~MessagePredicate() = default;
/**
* @brief Checks whether this predicate applies to the passed message
*
* Calls the derived classes `appliedTo` implementation, and respects the `isNegated_` flag
* it's set.
*
* @param message the message to check for this predicate
* @return true if this predicate applies, false otherwise
**/
bool appliesTo(const Message &message)
{
auto result = this->appliesToImpl(message);
if (this->isNegated_)
{
return !result;
}
return result;
}
protected:
explicit MessagePredicate(bool negate)
: isNegated_(negate)
{
}
/**
* @brief Checks whether this predicate applies to the passed message.
*
* Implementations of `appliesTo` should never change the message's content
* Implementations of `appliesToImpl` should never change the message's content
* in order to be compatible with other MessagePredicates.
*
* @param message the message to check for this predicate
* @return true if this predicate applies, false otherwise
*/
virtual bool appliesTo(const Message &message) = 0;
virtual bool appliesToImpl(const Message &message) = 0;
private:
const bool isNegated_ = false;
};
} // namespace chatterino

View file

@ -2,12 +2,13 @@
namespace chatterino {
RegexPredicate::RegexPredicate(const QString &regex)
: regex_(regex, QRegularExpression::CaseInsensitiveOption)
RegexPredicate::RegexPredicate(const QString &regex, bool negate)
: MessagePredicate(negate)
, regex_(regex, QRegularExpression::CaseInsensitiveOption)
{
}
bool RegexPredicate::appliesTo(const Message &message)
bool RegexPredicate::appliesToImpl(const Message &message)
{
if (!regex_.isValid())
{

View file

@ -20,9 +20,11 @@ public:
* The message is being matched case-insensitively.
*
* @param regex the regex to match the message against
* @param negate when set, excludes messages matching the regex from results
*/
RegexPredicate(const QString &regex);
RegexPredicate(const QString &regex, bool negate);
protected:
/**
* @brief Checks whether the message matches the regex passed in the
* constructor
@ -32,7 +34,7 @@ public:
* @param message the message to check
* @return true if the message matches the regex, false otherwise
*/
bool appliesTo(const Message &message);
bool appliesToImpl(const Message &message) override;
private:
/// Holds the regular expression to match the message against

View file

@ -3,11 +3,12 @@
namespace chatterino {
SubstringPredicate::SubstringPredicate(const QString &search)
: search_(search)
: MessagePredicate(false)
, search_(search)
{
}
bool SubstringPredicate::appliesTo(const Message &message)
bool SubstringPredicate::appliesToImpl(const Message &message)
{
return message.searchText.contains(this->search_, Qt::CaseInsensitive);
}

View file

@ -22,6 +22,7 @@ public:
*/
SubstringPredicate(const QString &search);
protected:
/**
* @brief Checks whether the message contains the substring passed in the
* constructor.
@ -31,7 +32,7 @@ public:
* @param message the message to check
* @return true if the message contains the substring, false otherwise
*/
bool appliesTo(const Message &message);
bool appliesToImpl(const Message &message) override;
private:
/// Holds the substring to search for in a message's `messageText`

View file

@ -4,19 +4,17 @@
namespace chatterino {
SubtierPredicate::SubtierPredicate(const QStringList &subtiers)
SubtierPredicate::SubtierPredicate(const QString &subtiers, bool negate)
: MessagePredicate(negate)
{
// Check if any comma-seperated values were passed and transform those
for (const auto &entry : subtiers)
{
for (const auto &subtier : entry.split(',', Qt::SkipEmptyParts))
for (const auto &subtier : subtiers.split(',', Qt::SkipEmptyParts))
{
this->subtiers_ << subtier;
}
}
}
bool SubtierPredicate::appliesTo(const Message &message)
bool SubtierPredicate::appliesToImpl(const Message &message)
{
for (const Badge &badge : message.badges)
{

View file

@ -16,10 +16,12 @@ public:
/**
* @brief Create an SubtierPredicate with a list of subtiers to search for.
*
* @param subtiers a list of subtiers that a message should contain
* @param subtiers one or more comma-separated subtiers that the message should contain
* @param negate when set, excludes messages containing selected subtiers from results
*/
SubtierPredicate(const QStringList &subtiers);
SubtierPredicate(const QString &subtiers, bool negate);
protected:
/**
* @brief Checks whether the message contains any of the subtiers passed
* in the constructor.
@ -28,7 +30,7 @@ public:
* @return true if the message contains a subtier listed in the specified subtiers,
* false otherwise
*/
bool appliesTo(const Message &message) override;
bool appliesToImpl(const Message &message) override;
private:
/// Holds the subtiers that will be searched for

View file

@ -302,55 +302,57 @@ std::vector<std::unique_ptr<MessagePredicate>> SearchPopup::parsePredicates(
// 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((?:(?<name>\w+):(?<value>".+?"|[^\s]+))|[^\s]+?(?=$|\s))lit");
R"lit((?<negation>[!\-])?(?:(?<name>\w+):(?<value>".+?"|[^\s]+))|[^\s]+?(?=$|\s))lit");
static QRegularExpression trimQuotationMarksRegex(R"(^"|"$)");
QRegularExpressionMatchIterator it = predicateRegex.globalMatch(input);
std::vector<std::unique_ptr<MessagePredicate>> predicates;
QStringList authors;
QStringList channels;
QStringList badges;
QStringList subtiers;
while (it.hasNext())
{
QRegularExpressionMatch match = it.next();
QString name = match.captured("name");
bool isNegated = !match.captured("negation").isEmpty();
QString value = match.captured("value");
value.remove(trimQuotationMarksRegex);
// match predicates
if (name == "from")
{
authors.append(value);
predicates.push_back(
std::make_unique<AuthorPredicate>(value, isNegated));
}
else if (name == "badge")
{
badges.append(value);
predicates.push_back(
std::make_unique<BadgePredicate>(value, isNegated));
}
else if (name == "subtier")
{
subtiers.append(value);
predicates.push_back(
std::make_unique<SubtierPredicate>(value, isNegated));
}
else if (name == "has" && value == "link")
{
predicates.push_back(std::make_unique<LinkPredicate>());
predicates.push_back(std::make_unique<LinkPredicate>(isNegated));
}
else if (name == "in")
{
channels.append(value);
predicates.push_back(
std::make_unique<ChannelPredicate>(value, isNegated));
}
else if (name == "is")
{
predicates.push_back(
std::make_unique<MessageFlagsPredicate>(value));
std::make_unique<MessageFlagsPredicate>(value, isNegated));
}
else if (name == "regex")
{
predicates.push_back(std::make_unique<RegexPredicate>(value));
predicates.push_back(
std::make_unique<RegexPredicate>(value, isNegated));
}
else
{
@ -359,26 +361,6 @@ std::vector<std::unique_ptr<MessagePredicate>> SearchPopup::parsePredicates(
}
}
if (!authors.empty())
{
predicates.push_back(std::make_unique<AuthorPredicate>(authors));
}
if (!channels.empty())
{
predicates.push_back(std::make_unique<ChannelPredicate>(channels));
}
if (!badges.empty())
{
predicates.push_back(std::make_unique<BadgePredicate>(badges));
}
if (!subtiers.empty())
{
predicates.push_back(std::make_unique<SubtierPredicate>(subtiers));
}
return predicates;
}