diff --git a/src/libs/utils/fuzzymatcher.cpp b/src/libs/utils/fuzzymatcher.cpp index f4362444c40..7b4d02bdf5c 100644 --- a/src/libs/utils/fuzzymatcher.cpp +++ b/src/libs/utils/fuzzymatcher.cpp @@ -17,7 +17,7 @@ * \return the regexp */ QRegularExpression FuzzyMatcher::createRegExp( - const QString &pattern, FuzzyMatcher::CaseSensitivity caseSensitivity) + const QString &pattern, FuzzyMatcher::CaseSensitivity caseSensitivity, bool multiWord) { if (pattern.isEmpty()) return QRegularExpression(); @@ -34,9 +34,10 @@ QRegularExpression FuzzyMatcher::createRegExp( * upper-case character. And any sequence of lower-case or upper case characters - * followed by an underscore can preceed a lower-case character. * - * Examples: (case sensitive mode) - * gAC matches getActionController - * gac matches get_action_controller + * Examples: + * gAC matches getActionController (case sensitive mode) + * gac matches get_action_controller (case sensitive mode) + * gac matches Get Action Container (case insensitive multi word mode) * * It also implements the fully and first-letter-only case sensitivity. */ @@ -51,6 +52,10 @@ QRegularExpression FuzzyMatcher::createRegExp( const QLatin1String uppercaseWordContinuation("[a-z0-9_]*"); const QLatin1String lowercaseWordContinuation("(?:[a-zA-Z0-9]*_)?"); const QLatin1String upperSnakeWordContinuation("[A-Z0-9]*_?"); + + const QLatin1String multiWordFirst("\\b"); + const QLatin1String multiWordContinuation("(?:.*?\\b)*?"); + keyRegExp += "(?:"; for (const QChar &c : pattern) { if (!c.isLetterOrNumber()) { @@ -60,6 +65,9 @@ QRegularExpression FuzzyMatcher::createRegExp( } else if (c == asterisk) { keyRegExp += ".*"; plainRegExp += ").*("; + } else if (multiWord && c == QChar::Space) { + // ignore spaces in keyRegExp + plainRegExp += QRegularExpression::escape(c); } else { const QString escaped = QRegularExpression::escape(c); keyRegExp += '(' + escaped + ')'; @@ -67,26 +75,34 @@ QRegularExpression FuzzyMatcher::createRegExp( } } else if (caseSensitivity == CaseSensitivity::CaseInsensitive || (caseSensitivity == CaseSensitivity::FirstLetterCaseSensitive && !first)) { - const QString upper = QRegularExpression::escape(c.toUpper()); const QString lower = QRegularExpression::escape(c.toLower()); - keyRegExp += "(?:"; - keyRegExp += first ? uppercaseWordFirst : uppercaseWordContinuation; - keyRegExp += '(' + upper + ')'; - if (first) { - keyRegExp += '|' + lowercaseWordFirst + '(' + lower + ')'; + if (multiWord) { + keyRegExp += first ? multiWordFirst : multiWordContinuation; + keyRegExp += '(' + upper + '|' + lower + ')'; } else { - keyRegExp += '|' + lowercaseWordContinuation + '(' + lower + ')'; - keyRegExp += '|' + upperSnakeWordContinuation + '(' + upper + ')'; + keyRegExp += "(?:"; + keyRegExp += first ? uppercaseWordFirst : uppercaseWordContinuation; + keyRegExp += '(' + upper + ')'; + if (first) { + keyRegExp += '|' + lowercaseWordFirst + '(' + lower + ')'; + } else { + keyRegExp += '|' + lowercaseWordContinuation + '(' + lower + ')'; + keyRegExp += '|' + upperSnakeWordContinuation + '(' + upper + ')'; + } + keyRegExp += ')'; } - keyRegExp += ')'; plainRegExp += '[' + upper + lower + ']'; } else { if (!first) { + if (multiWord) + keyRegExp += multiWordContinuation; if (c.isUpper()) keyRegExp += uppercaseWordContinuation; else keyRegExp += lowercaseWordContinuation; + } else if (multiWord) { + keyRegExp += multiWordFirst; } const QString escaped = QRegularExpression::escape(c); keyRegExp += escaped; @@ -106,13 +122,14 @@ QRegularExpression FuzzyMatcher::createRegExp( Qt::CaseSensitivity. */ QRegularExpression FuzzyMatcher::createRegExp(const QString &pattern, - Qt::CaseSensitivity caseSensitivity) + Qt::CaseSensitivity caseSensitivity, + bool multiWord) { const CaseSensitivity sensitivity = (caseSensitivity == Qt::CaseSensitive) ? CaseSensitivity::CaseSensitive : CaseSensitivity::CaseInsensitive; - return createRegExp(pattern, sensitivity); + return createRegExp(pattern, sensitivity, multiWord); } /*! diff --git a/src/libs/utils/fuzzymatcher.h b/src/libs/utils/fuzzymatcher.h index 89acaf018de..be9ac2fd9b1 100644 --- a/src/libs/utils/fuzzymatcher.h +++ b/src/libs/utils/fuzzymatcher.h @@ -30,9 +30,12 @@ public: QVector lengths; }; + static QRegularExpression createRegExp( + const QString &pattern, + CaseSensitivity caseSensitivity = CaseSensitivity::CaseInsensitive, + bool multiWord = false); static QRegularExpression createRegExp(const QString &pattern, - CaseSensitivity caseSensitivity = CaseSensitivity::CaseInsensitive); - static QRegularExpression createRegExp(const QString &pattern, - Qt::CaseSensitivity caseSensitivity); + Qt::CaseSensitivity caseSensitivity, + bool multiWord); static HighlightingPositions highlightingPositions(const QRegularExpressionMatch &match); }; diff --git a/src/plugins/coreplugin/locator/ilocatorfilter.cpp b/src/plugins/coreplugin/locator/ilocatorfilter.cpp index 4aaad9e4b69..28c7a86078f 100644 --- a/src/plugins/coreplugin/locator/ilocatorfilter.cpp +++ b/src/plugins/coreplugin/locator/ilocatorfilter.cpp @@ -224,11 +224,14 @@ Qt::CaseSensitivity ILocatorFilter::caseSensitivity(const QString &str) /*! Creates the search term \a text as a regular expression with case - sensitivity set to \a caseSensitivity. + sensitivity set to \a caseSensitivity. Pass true to \a multiWord if the pattern is + expected to contain spaces. */ -QRegularExpression ILocatorFilter::createRegExp(const QString &text, Qt::CaseSensitivity caseSensitivity) +QRegularExpression ILocatorFilter::createRegExp(const QString &text, + Qt::CaseSensitivity caseSensitivity, + bool multiWord) { - return FuzzyMatcher::createRegExp(text, caseSensitivity); + return FuzzyMatcher::createRegExp(text, caseSensitivity, multiWord); } /*! diff --git a/src/plugins/coreplugin/locator/ilocatorfilter.h b/src/plugins/coreplugin/locator/ilocatorfilter.h index e27b4618d80..58979d014bd 100644 --- a/src/plugins/coreplugin/locator/ilocatorfilter.h +++ b/src/plugins/coreplugin/locator/ilocatorfilter.h @@ -168,7 +168,8 @@ public: static Qt::CaseSensitivity caseSensitivity(const QString &str); static QRegularExpression createRegExp(const QString &text, - Qt::CaseSensitivity caseSensitivity = Qt::CaseInsensitive); + Qt::CaseSensitivity caseSensitivity = Qt::CaseInsensitive, + bool multiWord = false); static LocatorFilterEntry::HighlightInfo highlightInfo(const QRegularExpressionMatch &match, LocatorFilterEntry::HighlightInfo::DataType dataType = LocatorFilterEntry::HighlightInfo::DisplayName); diff --git a/tests/auto/utils/fuzzymatcher/tst_fuzzymatcher.cpp b/tests/auto/utils/fuzzymatcher/tst_fuzzymatcher.cpp index 19053ce259f..167db1b2660 100644 --- a/tests/auto/utils/fuzzymatcher/tst_fuzzymatcher.cpp +++ b/tests/auto/utils/fuzzymatcher/tst_fuzzymatcher.cpp @@ -13,8 +13,12 @@ class tst_FuzzyMatcher : public QObject private slots: void fuzzyMatcher(); void fuzzyMatcher_data(); + void fuzzyMatcherMultiWord(); + void fuzzyMatcherMultiWord_data(); void highlighting(); void highlighting_data(); + void highlightingMultiWord(); + void highlightingMultiWord_data(); }; void tst_FuzzyMatcher::fuzzyMatcher() @@ -64,7 +68,38 @@ void tst_FuzzyMatcher::fuzzyMatcher_data() QTest::newRow("middle-no-hump") << "window" << "mainwindow.cpp" << 4; QTest::newRow("case-insensitive") << "window" << "MAINWINDOW.cpp" << 4; QTest::newRow("case-insensitive-2") << "wINDow" << "MainwiNdow.cpp" << 4; - QTest::newRow("uppercase-word-and-humps") << "htvideoele" << "HTMLVideoElement" << 0; +} + +void tst_FuzzyMatcher::fuzzyMatcherMultiWord() +{ + QFETCH(QString, pattern); + QFETCH(QString, candidate); + QFETCH(int, expectedIndex); + + const QRegularExpression regExp + = FuzzyMatcher::createRegExp(pattern, FuzzyMatcher::CaseSensitivity::CaseInsensitive, true); + const QRegularExpressionMatch match = regExp.match(candidate); + QCOMPARE(match.capturedStart(), expectedIndex); +} + +void tst_FuzzyMatcher::fuzzyMatcherMultiWord_data() +{ + QTest::addColumn("pattern"); + QTest::addColumn("candidate"); + QTest::addColumn("expectedIndex"); + + QTest::newRow("one_word") << "fo" << "foo" << 0; + QTest::newRow("one_word_complete") << "foo" << "foo" << 0; + QTest::newRow("one_word_mismatch") << "bar" << "foo" << -1; + QTest::newRow("two_words") << "fb" << "foo bar" << 0; + QTest::newRow("two_wordslU") << "fb" << "Foo Bar" << 0; + QTest::newRow("two_wordsUl") << "FB" << "foo bar" << 0; + QTest::newRow("two_words_one_match") << "ba" << "foo bar" << 4; + QTest::newRow("two_words_complete_match") << "foo bar" << "foo bar" << 0; + QTest::newRow("wrong_order") << "bf" << "foo bar" << -1; + QTest::newRow("no_space") << "fb" << "foobar" << -1; + QTest::newRow("inword_first_match") << "oob" << "foo bar" << -1; + QTest::newRow("inword_second_match") << "foar" << "foo bar" << -1; } typedef QVector> Matches; @@ -142,5 +177,57 @@ void tst_FuzzyMatcher::highlighting_data() << Matches{{0, 2}, {4, 8}}; } +void tst_FuzzyMatcher::highlightingMultiWord() +{ + QFETCH(QString, pattern); + QFETCH(QString, candidate); + QFETCH(Matches, matches); + + const QRegularExpression regExp + = FuzzyMatcher::createRegExp(pattern, FuzzyMatcher::CaseSensitivity::CaseInsensitive, true); + const QRegularExpressionMatch match = regExp.match(candidate); + const FuzzyMatcher::HighlightingPositions positions + = FuzzyMatcher::highlightingPositions(match); + + QCOMPARE(positions.starts.size(), matches.size()); + for (int i = 0; i < positions.starts.size(); ++i) { + const QPair &match = matches.at(i); + QCOMPARE(positions.starts.at(i), match.first); + QCOMPARE(positions.lengths.at(i), match.second); + } +} + +void tst_FuzzyMatcher::highlightingMultiWord_data() +{ + QTest::addColumn("pattern"); + QTest::addColumn("candidate"); + QTest::addColumn("matches"); + + QTest::newRow("one_word") << "fo" + << "foo" << Matches{{0, 2}}; + QTest::newRow("one_word_complete") << "foo" + << "foo" << Matches{{0, 3}}; + QTest::newRow("one_word_mismatch") << "bar" + << "foo" << Matches{}; + QTest::newRow("two_words") << "fb" + << "foo bar" << Matches{{0, 1}, {4, 1}}; + QTest::newRow("two_wordslU") << "fb" + << "Foo Bar" << Matches{{0, 1}, {4, 1}}; + QTest::newRow("two_wordsUl") << "FB" + << "foo bar" << Matches{{0, 1}, {4, 1}}; + QTest::newRow("two_words_one_match") << "ba" + << "foo bar" << Matches{{4, 2}}; + QTest::newRow("two_words_complete_match") << "foo bar" + << "foo bar" << Matches{{0, 7}}; + QTest::newRow("wrong_order") << "bf" + << "foo bar" << Matches{}; + QTest::newRow("no_space") << "fb" + << "foobar" << Matches{}; + QTest::newRow("inword_first_match") << "oob" + << "foo bar" << Matches{}; + QTest::newRow("inword_second_match") << "foar" + << "foo bar" << Matches{}; +} + QTEST_APPLESS_MAIN(tst_FuzzyMatcher) #include "tst_fuzzymatcher.moc"