[Card] Bind search language per FilterString instance

The peg parser rules are set up once per process, so the GenericQuery and
OracleQuery rule actions could not capture per-instance state. Instead of
storing the search language in a process-global that FilterString instance
methods mutate, hand it to the rule actions through a thread-local parse
context and copy it into the filter closures they produce. Card evaluation
in FilterString::check no longer reads any process-global state, and each
instance keeps the language it was built with; constructing one instance no
longer changes what unrelated instances (deck filter, drop-to-hand, zone
views) match against.

The card database display model stores the raw query and rebuilds the
FilterString when the search language changes, since the language is now
bound at parse time.

Add tests for the English/Selected/Both search modes, the English fallback
for untranslated cards, and per-instance language independence.
This commit is contained in:
Lukas Brübach 2026-09-19 10:47:24 +02:00
parent e029173e29
commit 14cf4ee2a9
5 changed files with 81 additions and 33 deletions

View file

@ -74,13 +74,18 @@ NumericValue <- [0-9]+
static std::once_flag init;
// The peg parser rules are set up once per process, which means the GenericQuery/
// OracleQuery lambdas cannot capture per-instance state. The card language the
// plain-text name and text queries search in is therefore kept here and applied
// by those lambdas; every FilterString shares it because it reflects a single
// global user setting.
static QString globalSearchLanguage;
static CardSearchLanguage globalSearchLanguageMode = CardSearchLanguage::English;
// The peglib parser rules (and therefore their rule actions) are set up once per
// process, so a rule action cannot capture per-instance state. The card language
// plain-text name and text queries search in is therefore handed to the GenericQuery
// and OracleQuery rule actions through this thread-local context, which is live only
// while a FilterString is being parsed. The rule actions copy it into the filter
// closures they produce, so card evaluation never reads process-global state.
struct SearchLanguageContext
{
QString searchLanguage;
CardSearchLanguage searchLanguageMode = CardSearchLanguage::English;
};
thread_local SearchLanguageContext searchLanguageContext;
namespace
{
@ -365,9 +370,11 @@ static void setupParserRules()
search["OracleQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto matcher = std::any_cast<StringMatcher>(sv[0]);
const QString searchLanguage = searchLanguageContext.searchLanguage;
const CardSearchLanguage searchLanguageMode = searchLanguageContext.searchLanguageMode;
return [=](const CardData &x) {
return matchesInSearchLanguage(x->getText(), x->getLocalizedText(globalSearchLanguage),
globalSearchLanguage, globalSearchLanguageMode, matcher);
return matchesInSearchLanguage(x->getText(), x->getLocalizedText(searchLanguage), searchLanguage,
searchLanguageMode, matcher);
};
};
@ -445,9 +452,11 @@ static void setupParserRules()
};
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto matcher = std::any_cast<StringMatcher>(sv[0]);
const QString searchLanguage = searchLanguageContext.searchLanguage;
const CardSearchLanguage searchLanguageMode = searchLanguageContext.searchLanguageMode;
return [=](const CardData &x) {
return matchesInSearchLanguage(x->getName(), x->getLocalizedName(globalSearchLanguage),
globalSearchLanguage, globalSearchLanguageMode, matcher);
return matchesInSearchLanguage(x->getName(), x->getLocalizedName(searchLanguage), searchLanguage,
searchLanguageMode, matcher);
};
};
@ -463,7 +472,7 @@ FilterString::FilterString()
_error = "Not initialized";
}
FilterString::FilterString(const QString &expr)
FilterString::FilterString(const QString &expr, const QString &searchLanguage, CardSearchLanguage searchLanguageMode)
{
QByteArray ba = expr.simplified().toUtf8();
@ -476,6 +485,8 @@ FilterString::FilterString(const QString &expr)
return;
}
searchLanguageContext = SearchLanguageContext{searchLanguage, searchLanguageMode};
search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) {
_error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg));
});
@ -485,9 +496,3 @@ FilterString::FilterString(const QString &expr)
result = [](const CardData &) -> bool { return false; };
}
}
void FilterString::setSearchLanguage(const QString &searchLanguage, CardSearchLanguage searchLanguageMode)
{
globalSearchLanguage = searchLanguage;
globalSearchLanguageMode = searchLanguageMode;
}

View file

@ -36,7 +36,9 @@ class FilterString
{
public:
FilterString();
explicit FilterString(const QString &exp);
explicit FilterString(const QString &exp,
const QString &searchLanguage = QString(),
CardSearchLanguage searchLanguageMode = CardSearchLanguage::English);
[[nodiscard]] bool check(const CardData &card) const
{
if (card.isNull()) {
@ -56,17 +58,6 @@ public:
return _error;
}
/**
* @brief Sets the card language plain-text name and text queries run against.
*
* The peg parser rules are set up once per process, so this propagates to
* every FilterString instance created through the shared parser.
*
* @param searchLanguage Empty string for English only, otherwise the card language code.
* @param searchLanguageMode The search language mode from CardSearchLanguage.
*/
void setSearchLanguage(const QString &searchLanguage, CardSearchLanguage searchLanguageMode);
private:
QString _error;
Filter result;

View file

@ -239,9 +239,9 @@ void CardDatabaseDisplayModel::setFilterTree(FilterTree *_filterTree)
void CardDatabaseDisplayModel::setStringFilter(const QString &_src)
{
searchText = _src;
delete filterString;
filterString = new FilterString(_src);
filterString->setSearchLanguage(searchLanguage, searchLanguageMode);
filterString = new FilterString(_src, searchLanguage, searchLanguageMode);
dirty();
}
@ -255,7 +255,7 @@ void CardDatabaseDisplayModel::setSearchLanguage(const QString &searchLang, Card
searchLanguageMode = mode;
if (filterString != nullptr) {
filterString->setSearchLanguage(searchLanguage, searchLanguageMode);
setStringFilter(searchText);
}
dirty();
}

View file

@ -35,6 +35,7 @@ private:
QTimer dirtyTimer;
QString searchLanguage;
CardSearchLanguage searchLanguageMode = CardSearchLanguage::English;
QString searchText;
/** The translation table that will be used for sanitizeCardName. */
static QMap<wchar_t, wchar_t> characterTranslation;

View file

@ -73,6 +73,57 @@ QUERY(Color4, cat, "c!gw", false)
QUERY(BracketNextToUnquotedString, cat, "(o:woof OR o:meow)", true)
CardInfoPtr localizedCat()
{
CardInfoPtr localized = CardInfo::newInstance("Cat", "Meow!", false, {}, {}, {}, {}, {});
localized->setLocalizedName("de", "Kater");
localized->setLocalizedText("de", "miaut");
return localized;
}
TEST_F(CardQuery, SearchLanguageEnglishMatchesOnlyEnglish)
{
const CardData localized = localizedCat();
ASSERT_TRUE(FilterString("Cat", "de", CardSearchLanguage::English).check(localized));
ASSERT_FALSE(FilterString("Kater", "de", CardSearchLanguage::English).check(localized));
}
TEST_F(CardQuery, SearchLanguageSelectedMatchesLocalizedNameAndText)
{
const CardData localized = localizedCat();
ASSERT_TRUE(FilterString("Kater", "de", CardSearchLanguage::Selected).check(localized));
ASSERT_TRUE(FilterString("o:miaut", "de", CardSearchLanguage::Selected).check(localized));
ASSERT_FALSE(FilterString("Cat", "de", CardSearchLanguage::Selected).check(localized));
}
TEST_F(CardQuery, SearchLanguageSelectedFallsBackToEnglishForUntranslatedCards)
{
const CardData localized = localizedCat();
ASSERT_TRUE(FilterString("Cat", "fr", CardSearchLanguage::Selected).check(localized));
ASSERT_FALSE(FilterString("Kater", "fr", CardSearchLanguage::Selected).check(localized));
}
TEST_F(CardQuery, SearchLanguageBothMatchesEitherLanguage)
{
const CardData localized = localizedCat();
ASSERT_TRUE(FilterString("Cat", "de", CardSearchLanguage::Both).check(localized));
ASSERT_TRUE(FilterString("Kater", "de", CardSearchLanguage::Both).check(localized));
}
TEST_F(CardQuery, SearchLanguageIsBoundPerInstance)
{
const CardData localized = localizedCat();
FilterString germanQuery("Kater", "de", CardSearchLanguage::Selected);
ASSERT_TRUE(germanQuery.check(localized));
// Constructing an English-bound instance afterwards must not change the
// language the earlier instance searches in.
FilterString englishQuery("Kater", "", CardSearchLanguage::English);
ASSERT_FALSE(englishQuery.check(localized));
ASSERT_TRUE(germanQuery.check(localized));
}
} // namespace
int main(int argc, char **argv)