mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
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:
parent
4fa214a38a
commit
b7888749fe
19 changed files with 139 additions and 111 deletions
|
@ -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)
|
||||
|
|
|
@ -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 : authors.split(',', Qt::SkipEmptyParts))
|
||||
{
|
||||
for (const auto &author : entry.split(',', Qt::SkipEmptyParts))
|
||||
{
|
||||
this->authors_ << author;
|
||||
}
|
||||
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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,35 +4,33 @@
|
|||
|
||||
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 : badges.split(',', Qt::SkipEmptyParts))
|
||||
{
|
||||
for (const auto &badge : entry.split(',', Qt::SkipEmptyParts))
|
||||
// convert short form name of certain badges to formal name
|
||||
if (badge.compare("mod", Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
// convert short form name of certain badges to formal name
|
||||
if (badge.compare("mod", Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
this->badges_ << "moderator";
|
||||
}
|
||||
else if (badge.compare("sub", Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
this->badges_ << "subscriber";
|
||||
}
|
||||
else if (badge.compare("prime", Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
this->badges_ << "premium";
|
||||
}
|
||||
else
|
||||
{
|
||||
this->badges_ << badge;
|
||||
}
|
||||
this->badges_ << "moderator";
|
||||
}
|
||||
else if (badge.compare("sub", Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
this->badges_ << "subscriber";
|
||||
}
|
||||
else if (badge.compare("prime", Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
this->badges_ << "premium";
|
||||
}
|
||||
else
|
||||
{
|
||||
this->badges_ << badge;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool BadgePredicate::appliesTo(const Message &message)
|
||||
bool BadgePredicate::appliesToImpl(const Message &message)
|
||||
{
|
||||
for (const Badge &badge : message.badges)
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 : channels.split(',', Qt::SkipEmptyParts))
|
||||
{
|
||||
for (const auto &channel : entry.split(',', Qt::SkipEmptyParts))
|
||||
{
|
||||
this->channels_ << channel;
|
||||
}
|
||||
this->channels_ << channel;
|
||||
}
|
||||
}
|
||||
|
||||
bool ChannelPredicate::appliesTo(const Message &message)
|
||||
bool ChannelPredicate::appliesToImpl(const Message &message)
|
||||
{
|
||||
return channels_.contains(message.channelName, Qt::CaseInsensitive);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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_);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
RegexPredicate::RegexPredicate(const QString ®ex)
|
||||
: regex_(regex, QRegularExpression::CaseInsensitiveOption)
|
||||
RegexPredicate::RegexPredicate(const QString ®ex, bool negate)
|
||||
: MessagePredicate(negate)
|
||||
, regex_(regex, QRegularExpression::CaseInsensitiveOption)
|
||||
{
|
||||
}
|
||||
|
||||
bool RegexPredicate::appliesTo(const Message &message)
|
||||
bool RegexPredicate::appliesToImpl(const Message &message)
|
||||
{
|
||||
if (!regex_.isValid())
|
||||
{
|
||||
|
@ -19,4 +20,4 @@ bool RegexPredicate::appliesTo(const Message &message)
|
|||
return match.hasMatch();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -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 ®ex);
|
||||
RegexPredicate(const QString ®ex, bool negate);
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Checks whether the message matches the regex passed in the
|
||||
* constructor
|
||||
|
@ -32,11 +34,11 @@ 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
|
||||
QRegularExpression regex_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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 : subtiers.split(',', Qt::SkipEmptyParts))
|
||||
{
|
||||
for (const auto &subtier : entry.split(',', Qt::SkipEmptyParts))
|
||||
{
|
||||
this->subtiers_ << subtier;
|
||||
}
|
||||
this->subtiers_ << subtier;
|
||||
}
|
||||
}
|
||||
|
||||
bool SubtierPredicate::appliesTo(const Message &message)
|
||||
bool SubtierPredicate::appliesToImpl(const Message &message)
|
||||
{
|
||||
for (const Badge &badge : message.badges)
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue