diff --git a/kdevplatform/language/interfaces/abbreviations.cpp b/kdevplatform/language/interfaces/abbreviations.cpp index 41a7243278..6d1c5fc30b 100644 --- a/kdevplatform/language/interfaces/abbreviations.cpp +++ b/kdevplatform/language/interfaces/abbreviations.cpp @@ -1,218 +1,223 @@ /* This file is part of KDevelop Copyright 2014 Sven Brauch This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "abbreviations.h" #include #include namespace KDevelop { bool matchesAbbreviationHelper(const QStringRef &word, const QString &typed, const QVarLengthArray< int, 32 > &offsets, int &depth, int atWord, int i) { int atLetter = 1; for ( ; i < typed.size(); i++ ) { const QChar c = typed.at(i).toLower(); bool haveNextWord = offsets.size() > atWord + 1; bool canCompare = atWord != -1 && word.size() > offsets.at(atWord) + atLetter; if ( canCompare && c == word.at(offsets.at(atWord) + atLetter).toLower() ) { // the typed letter matches a letter after the current word beginning if ( ! haveNextWord || c != word.at(offsets.at(atWord + 1)).toLower() ) { // good, simple case, no conflict atLetter += 1; continue; } // For maliciously crafted data, the code used here theoretically can have very high // complexity. Thus ensure we don't run into this case, by limiting the amount of branches // we walk through to 128. depth++; if ( depth > 128 ) { return false; } // the letter matches both the next word beginning and the next character in the word if ( haveNextWord && matchesAbbreviationHelper(word, typed, offsets, depth, atWord + 1, i + 1) ) { // resolving the conflict by taking the next word's first character worked, fine return true; } // otherwise, continue by taking the next letter in the current word. atLetter += 1; continue; } else if ( haveNextWord && c == word.at(offsets.at(atWord + 1)).toLower() ) { // the typed letter matches the next word beginning atWord++; atLetter = 1; continue; } // no match return false; } // all characters of the typed word were matched return true; } bool matchesAbbreviation(const QStringRef &word, const QString &typed) { // A mismatch is very likely for random even for the first letter, // thus this optimization makes sense. if ( word.at(0).toLower() != typed.at(0).toLower() ) { return false; } // First, check if all letters are contained in the word in the right order. int atLetter = 0; foreach ( const QChar c, typed ) { while ( c.toLower() != word.at(atLetter).toLower() ) { atLetter += 1; if ( atLetter >= word.size() ) { return false; } } } bool haveUnderscore = true; QVarLengthArray offsets; // We want to make "KComplM" match "KateCompletionModel"; this means we need // to allow parts of the typed text to be not part of the actual abbreviation, // which consists only of the uppercased / underscored letters (so "KCM" in this case). // However it might be ambigous whether a letter is part of such a word or part of // the following abbreviation, so we need to find all possible word offsets first, // then compare. for ( int i = 0; i < word.size(); i++ ) { const QChar c = word.at(i); if ( c == QLatin1Char('_') || c == QLatin1Char('-') ) { haveUnderscore = true; } else if ( haveUnderscore || c.isUpper() ) { offsets.append(i); haveUnderscore = false; } } int depth = 0; return matchesAbbreviationHelper(word, typed, offsets, depth); } bool matchesPath(const QString &path, const QString &typed) { int consumed = 0; int pos = 0; // try to find all the characters in typed in the right order in the path; // jumps are allowed everywhere while ( consumed < typed.size() && pos < path.size() ) { if ( typed.at(consumed).toLower() == path.at(pos).toLower() ) { consumed++; } pos++; } return consumed == typed.size(); } bool matchesAbbreviationMulti(const QString &word, const QStringList &typedFragments) { if ( word.size() == 0 ) { return true; } int lastSpace = 0; int matchedFragments = 0; for ( int i = 0; i < word.size(); i++ ) { const QChar& c = word.at(i); bool isDoubleColon = false; // if it's not a separation char, walk over it. if (c != QLatin1Char(' ') && c != QLatin1Char('/') && i != word.size() - 1) { if (c != QLatin1Char(':') && i < word.size()-1 && word.at(i+1) != QLatin1Char(':')) { continue; } isDoubleColon = true; i++; } // if it's '/', ' ' or '::', split the word here and check the next sub-word. const QStringRef wordFragment = word.midRef(lastSpace, i-lastSpace); const QString& typedFragment = typedFragments.at(matchedFragments); Q_ASSERT(!typedFragment.isEmpty()); if ( !wordFragment.isEmpty() && matchesAbbreviation(wordFragment, typedFragment) ) { matchedFragments += 1; if ( matchedFragments == typedFragments.size() ) { break; } } lastSpace = isDoubleColon ? i : i+1; } return matchedFragments == typedFragments.size(); } -PathFilterMatchQuality matchPathFilter(const Path &toFilter, const QStringList &text, - const QString &joinedText) +PathFilterMatchQuality matchPathFilter(const Path &toFilter, const QStringList &text) { const QVector& segments = toFilter.segments(); if (text.count() > segments.count()) { // number of segments mismatches, thus item cannot match return PathFilterMatchQuality::NoMatch; } { bool allMatched = true; // try to put exact matches up front for(int i = segments.count() - 1, j = text.count() - 1; i >= 0 && j >= 0; --i, --j) { if (segments.at(i) != text.at(j)) { allMatched = false; break; } } if (allMatched) { return PathFilterMatchQuality::ExactMatch; } } int searchIndex = 0; int pathIndex = 0; int lastMatchIndex = -1; // stop early if more search fragments remain than available after path index while (pathIndex < segments.size() && searchIndex < text.size() && (pathIndex + text.size() - searchIndex - 1) < segments.size() ) { const QString& segment = segments.at(pathIndex); const QString& typedSegment = text.at(searchIndex); lastMatchIndex = segment.indexOf(typedSegment, 0, Qt::CaseInsensitive); - if (lastMatchIndex == -1 && !matchesAbbreviation(segment.midRef(0), typedSegment)) { + bool isMatch = lastMatchIndex != -1; + // do fuzzy path matching on the last segment + if (!isMatch && searchIndex == text.size() - 1 && pathIndex == segments.size() - 1) { + isMatch = matchesPath(segment, typedSegment); + } else if (!isMatch) { + isMatch = matchesAbbreviation(segment.midRef(0), typedSegment); + } + + if (!isMatch) { // no match, try with next path segment ++pathIndex; continue; } // else we matched ++searchIndex; ++pathIndex; } if (searchIndex != text.size()) { - if ( ! matchesPath(segments.last(), joinedText) ) { - return PathFilterMatchQuality::NoMatch; - } + return PathFilterMatchQuality::NoMatch; } // prefer matches whose last element starts with the filter if (pathIndex == segments.size() && lastMatchIndex == 0) { return PathFilterMatchQuality::StartMatch; } else { return PathFilterMatchQuality::OtherMatch; } } } // namespace KDevelop // kate: space-indent on; indent-width 2 diff --git a/kdevplatform/language/interfaces/abbreviations.h b/kdevplatform/language/interfaces/abbreviations.h index 7659d6b17d..ae611abbc4 100644 --- a/kdevplatform/language/interfaces/abbreviations.h +++ b/kdevplatform/language/interfaces/abbreviations.h @@ -1,70 +1,69 @@ /* This file is part of KDevelop Copyright 2014 Sven Brauch This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_ABBREVIATIONS_H #define KDEVPLATFORM_ABBREVIATIONS_H #include #include class QStringList; class QStringRef; class QString; namespace KDevelop { class Path; // Taken and adapted for kdevelop from katecompletionmodel.cpp KDEVPLATFORMLANGUAGE_EXPORT bool matchesAbbreviationHelper(const QStringRef& word, const QString& typed, const QVarLengthArray& offsets, int& depth, int atWord = -1, int i = 0); KDEVPLATFORMLANGUAGE_EXPORT bool matchesAbbreviation(const QStringRef& word, const QString& typed); KDEVPLATFORMLANGUAGE_EXPORT bool matchesPath(const QString& path, const QString& typed); /** * @brief Matches a word against a list of search fragments. * The word will be split at separation characters (space, / and ::) and * the resulting fragments will be matched one-by-one against the typed fragments. * If all typed fragments can be matched against a fragment in word in the right order * (skipping is allowed), true will be returned. * @param word the word to search in * @param typedFragments the fragments which were typed * @return bool true if match, else false */ KDEVPLATFORMLANGUAGE_EXPORT bool matchesAbbreviationMulti(const QString& word, const QStringList& typedFragments); enum class PathFilterMatchQuality { NoMatch, ExactMatch, StartMatch, OtherMatch }; /** * @brief Matches a path against a list of search fragments. */ -KDEVPLATFORMLANGUAGE_EXPORT PathFilterMatchQuality matchPathFilter(const Path& toFilter, const QStringList& text, - const QString& joinedText); +KDEVPLATFORMLANGUAGE_EXPORT PathFilterMatchQuality matchPathFilter(const Path& toFilter, const QStringList& text); } #endif // kate: space-indent on; indent-width 2 diff --git a/kdevplatform/language/interfaces/quickopenfilter.h b/kdevplatform/language/interfaces/quickopenfilter.h index 33285fdc67..6ee4a10f22 100644 --- a/kdevplatform/language/interfaces/quickopenfilter.h +++ b/kdevplatform/language/interfaces/quickopenfilter.h @@ -1,236 +1,233 @@ /* * This file is part of KDevelop * * Copyright 2007 David Nolden * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_QUICKOPEN_FILTER_H #define KDEVPLATFORM_QUICKOPEN_FILTER_H #include #include "abbreviations.h" #include namespace KDevelop { /** * This is a simple filter-implementation that helps you implementing own quickopen data-providers. * You should use it when possible, because that way additional features(like regexp filtering) can * be implemented in a central place. * * This implementation does incremental filtering while * typing text, so it quite efficient for the most common case. * * The simplest way of using this is by reimplementing your data-provider * based on QuickOpenDataProviderBase and KDevelop::Filter\. * * What you need to do to use it: * * Reimplement itemText(..) to provide the text filtering * should be performend on(This must be efficient). * * Call setItems(..) when starting a new quickopen session, or when the content * changes, to initialize the filter with your data. * * Call setFilter(..) with the text that should be filtered for on user-input. * * Use filteredItems() to provide data to quickopen. * * @tparam Item should be the type that holds all the information you need. * The filter will hold the data, and you can access it through "items()". */ template class Filter { public: virtual ~Filter() { } ///Clears the filter, but not the data. void clearFilter() { m_filtered = m_items; m_oldFilterText.clear(); } ///Clears the filter and sets new data. The filter-text will be lost. void setItems(const QVector& data) { m_items = data; clearFilter(); } const QVector& items() const { return m_items; } ///Returns the data that is left after the filtering const QVector& filteredItems() const { return m_filtered; } ///Changes the filter-text and refilters the data void setFilter( const QString& text ) { if (m_oldFilterText == text) { return; } if (text.isEmpty()) { clearFilter(); return; } QVector filterBase = m_filtered; if( !text.startsWith( m_oldFilterText ) ) { filterBase = m_items; //Start filtering based on the whole data } m_filtered.clear(); QStringList typedFragments = text.split(QStringLiteral("::"), QString::SkipEmptyParts); if (typedFragments.isEmpty()) { clearFilter(); return; } if ( typedFragments.last().endsWith(':') ) { // remove the trailing colon if there's only one; otherwise, // this breaks incremental filtering typedFragments.last().chop(1); } if (typedFragments.size() == 1 && typedFragments.last().isEmpty()) { clearFilter(); return; } foreach( const Item& data, filterBase ) { const QString& itemData = itemText( data ); if( itemData.contains(text, Qt::CaseInsensitive) || matchesAbbreviationMulti(itemData, typedFragments) ) { m_filtered << data; } } m_oldFilterText = text; } protected: ///Should return the text an item should be filtered by. virtual QString itemText( const Item& data ) const = 0; private: QString m_oldFilterText; QVector m_filtered; QVector m_items; }; template class PathFilter { public: ///Clears the filter, but not the data. void clearFilter() { m_filtered = m_items; m_oldFilterText.clear(); } ///Clears the filter and sets new data. The filter-text will be lost. void setItems(const QVector& data) { m_items = data; clearFilter(); } const QVector& items() const { return m_items; } ///Returns the data that is left after the filtering const QVector& filteredItems() const { return m_filtered; } ///Changes the filter-text and refilters the data void setFilter( const QStringList& text ) { if (m_oldFilterText == text) { return; } if (text.isEmpty()) { clearFilter(); return; } - const QString joinedText = text.join(QString()); - QVector filterBase = m_filtered; if ( m_oldFilterText.isEmpty()) { filterBase = m_items; } else if (m_oldFilterText.mid(0, m_oldFilterText.count() - 1) == text.mid(0, text.count() - 1) && text.last().startsWith(m_oldFilterText.last())) { //Good, the prefix is the same, and the last item has been extended } else if (m_oldFilterText.size() == text.size() - 1 && m_oldFilterText == text.mid(0, text.size() - 1)) { //Good, an item has been added } else { //Start filtering based on the whole data, there was a big change to the filter filterBase = m_items; } // filterBase is correctly sorted, to keep it that way we add // exact matches to this list in sorted way and then prepend the whole list in one go. QVector exactMatches; // similar for starting matches QVector startMatches; // all other matches are sorted by where they match, we prefer matches at the end QVector otherMatches; foreach( const Item& data, filterBase ) { - const auto matchQuality = matchPathFilter(static_cast(this)->itemPath(data), - text, joinedText); + const auto matchQuality = matchPathFilter(static_cast(this)->itemPath(data), text); switch (matchQuality) { case PathFilterMatchQuality::NoMatch: break; case PathFilterMatchQuality::ExactMatch: exactMatches << data; break; case PathFilterMatchQuality::StartMatch: startMatches << data; break; case PathFilterMatchQuality::OtherMatch: otherMatches << data; break; } } m_filtered = exactMatches + startMatches + otherMatches; m_oldFilterText = text; } private: QStringList m_oldFilterText; QVector m_filtered; QVector m_items; }; } #endif diff --git a/plugins/quickopen/tests/test_quickopen.cpp b/plugins/quickopen/tests/test_quickopen.cpp index bbce4c520d..a77813a501 100644 --- a/plugins/quickopen/tests/test_quickopen.cpp +++ b/plugins/quickopen/tests/test_quickopen.cpp @@ -1,300 +1,329 @@ /* * Copyright Milian Wolff * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_quickopen.h" #include #include #include #include QTEST_MAIN(TestQuickOpen); using namespace KDevelop; using ItemList = QVector; using StringList = QVector; TestQuickOpen::TestQuickOpen(QObject* parent) : QuickOpenTestBase(Core::Default, parent) { } void TestQuickOpen::testDuchainFilter() { QFETCH(ItemList, items); QFETCH(QString, filter); QFETCH(ItemList, filtered); auto toStringList = [](const ItemList& items) { QStringList result; for (const DUChainItem& item: items) { result << item.m_text; } return result; }; TestFilter filterItems; filterItems.setItems(items); filterItems.setFilter(filter); QCOMPARE(toStringList(filterItems.filteredItems()), toStringList(filtered)); } void TestQuickOpen::testDuchainFilter_data() { QTest::addColumn("items"); QTest::addColumn("filter"); QTest::addColumn("filtered"); auto i = [](const QString& text) { auto item = DUChainItem(); item.m_text = text; return item; }; auto items = ItemList() << i(QStringLiteral("KTextEditor::Cursor")) << i(QStringLiteral("void KTextEditor::Cursor::explode()")) << i(QStringLiteral("QVector SomeNamespace::SomeClass::func(int)")); QTest::newRow("prefix") << items << "KTE" << (ItemList() << items.at(0) << items.at(1)); QTest::newRow("prefix_mismatch") << items << "KTEY" << (ItemList()); QTest::newRow("prefix_colon") << items << "KTE:" << (ItemList() << items.at(0) << items.at(1)); QTest::newRow("prefix_colon_mismatch") << items << "KTE:Y" << (ItemList()); QTest::newRow("prefix_colon_mismatch2") << items << "XKTE:" << (ItemList()); QTest::newRow("prefix_two_colon") << items << "KTE::" << (ItemList() << items.at(0) << items.at(1)); QTest::newRow("prefix_two_colon_mismatch") << items << "KTE::Y" << (ItemList()); QTest::newRow("prefix_two_colon_mismatch2") << items << "XKTE::" << (ItemList()); QTest::newRow("suffix") << items << "Curs" << (ItemList() << items.at(0) << items.at(1)); QTest::newRow("suffix2") << items << "curs" << (ItemList() << items.at(0) << items.at(1)); QTest::newRow("mid") << items << "SomeClass" << (ItemList() << items.at(2)); QTest::newRow("mid_abbrev") << items << "SClass" << (ItemList() << items.at(2)); } void TestQuickOpen::testAbbreviations() { QFETCH(StringList, items); QFETCH(QString, filter); QFETCH(StringList, filtered); PathTestFilter filterItems; filterItems.setItems(items); filterItems.setFilter(filter.split('/', QString::SkipEmptyParts)); QCOMPARE(filterItems.filteredItems(), filtered); } void TestQuickOpen::testAbbreviations_data() { QTest::addColumn("items"); QTest::addColumn("filter"); QTest::addColumn("filtered"); const StringList items = { QStringLiteral("/foo/bar/caz/a.h"), QStringLiteral("/KateThing/CMakeLists.txt"), QStringLiteral("/FooBar/FooBar/Footestfoo.h") }; QTest::newRow("path_segments") << items << "fbc" << StringList(); QTest::newRow("path_segment_abbrev") << items << "cmli" << StringList({ items.at(1) }); QTest::newRow("path_segment_old") << items << "kate/cmake" << StringList({ items.at(1) }); QTest::newRow("path_segment_multi_mixed") << items << "ftfoo.h" << StringList({ items.at(2) }); } void TestQuickOpen::testSorting() { QFETCH(StringList, items); QFETCH(QString, filter); QFETCH(StringList, filtered); PathTestFilter filterItems; filterItems.setItems(items); filterItems.setFilter(filter.split('/', QString::SkipEmptyParts)); QEXPECT_FAIL("bar7", "empty parts are skipped", Continue); QCOMPARE(filterItems.filteredItems(), filtered); } void TestQuickOpen::testSorting_data() { QTest::addColumn("items"); QTest::addColumn("filter"); QTest::addColumn("filtered"); const StringList items({ QStringLiteral("/foo/a.h"), QStringLiteral("/foo/ab.h"), QStringLiteral("/foo/bc.h"), QStringLiteral("/bar/a.h")}); { QTest::newRow("no-filter") << items << QString() << items; } { const StringList filtered = { QStringLiteral("/bar/a.h") }; QTest::newRow("bar1") << items << QStringLiteral("bar") << filtered; QTest::newRow("bar2") << items << QStringLiteral("/bar") << filtered; QTest::newRow("bar3") << items << QStringLiteral("/bar/") << filtered; QTest::newRow("bar4") << items << QStringLiteral("bar/") << filtered; QTest::newRow("bar5") << items << QStringLiteral("ar/") << filtered; QTest::newRow("bar6") << items << QStringLiteral("r/") << filtered; QTest::newRow("bar7") << items << QStringLiteral("b/") << filtered; QTest::newRow("bar8") << items << QStringLiteral("b/a") << filtered; QTest::newRow("bar9") << items << QStringLiteral("b/a.h") << filtered; QTest::newRow("bar10") << items << QStringLiteral("b/a.") << filtered; } { const StringList filtered = { QStringLiteral("/foo/a.h"), QStringLiteral("/foo/ab.h") }; QTest::newRow("foo_a1") << items << QStringLiteral("foo/a") << filtered; QTest::newRow("foo_a2") << items << QStringLiteral("/f/a") << filtered; } { // now matches ab.h too because of abbreviation matching, but should be sorted last const StringList filtered = { QStringLiteral("/foo/a.h"), QStringLiteral("/bar/a.h"), QStringLiteral("/foo/ab.h") }; QTest::newRow("a_h") << items << QStringLiteral("a.h") << filtered; } { const StringList base = { QStringLiteral("/foo/a_test"), QStringLiteral("/foo/test_b_1"), QStringLiteral("/foo/test_b") }; const StringList sorted = { QStringLiteral("/foo/test_b"), QStringLiteral("/foo/test_b_1") }; QTest::newRow("prefer_exact") << base << QStringLiteral("test_b") << sorted; } { // from commit: 769491f06a4560a4798592ff060675ffb0d990a6 const QString file = QStringLiteral("/myProject/someStrangePath/anItem.cpp"); const StringList base = { QStringLiteral("/foo/a"), file }; const StringList filtered = { file }; QTest::newRow("strange") << base << QStringLiteral("strange/item") << filtered; } { const StringList base = { QStringLiteral("/foo/a_test"), QStringLiteral("/foo/test_b_1"), QStringLiteral("/foo/test_b"), QStringLiteral("/foo/test/a") }; const StringList sorted = { QStringLiteral("/foo/test_b_1"), QStringLiteral("/foo/test_b"), QStringLiteral("/foo/a_test"), QStringLiteral("/foo/test/a") }; QTest::newRow("prefer_start1") << base << QStringLiteral("test") << sorted; QTest::newRow("prefer_start2") << base << QStringLiteral("foo/test") << sorted; } { const StringList base = { QStringLiteral("/muh/kuh/asdf/foo"), QStringLiteral("/muh/kuh/foo/asdf") }; const StringList reverse = { QStringLiteral("/muh/kuh/foo/asdf"), QStringLiteral("/muh/kuh/asdf/foo") }; QTest::newRow("prefer_start3") << base << QStringLiteral("f") << base; QTest::newRow("prefer_start4") << base << QStringLiteral("/fo") << base; QTest::newRow("prefer_start5") << base << QStringLiteral("/foo") << base; QTest::newRow("prefer_start6") << base << QStringLiteral("a") << reverse; QTest::newRow("prefer_start7") << base << QStringLiteral("/a") << reverse; QTest::newRow("prefer_start8") << base << QStringLiteral("uh/as") << reverse; QTest::newRow("prefer_start9") << base << QStringLiteral("asdf") << reverse; } { QTest::newRow("duplicate") << StringList({ QStringLiteral("/muh/kuh/asdf/foo") }) << QStringLiteral("kuh/kuh") << StringList(); } + { + const StringList fuzzyItems = { + QStringLiteral("/foo/bar.h"), + QStringLiteral("/foo/fooXbar.h"), + QStringLiteral("/foo/fXoXoXbXaXr.h"), + QStringLiteral("/bar/FOOxBAR.h") + }; + + QTest::newRow("fuzzy1") << fuzzyItems << QStringLiteral("br") << fuzzyItems; + QTest::newRow("fuzzy2") << fuzzyItems << QStringLiteral("foo/br") << StringList({ + QStringLiteral("/foo/bar.h"), + QStringLiteral("/foo/fooXbar.h"), + QStringLiteral("/foo/fXoXoXbXaXr.h") + }); + QTest::newRow("fuzzy3") << fuzzyItems << QStringLiteral("b/br") << StringList({ + QStringLiteral("/bar/FOOxBAR.h") + }); + QTest::newRow("fuzzy4") << fuzzyItems << QStringLiteral("br/br") << StringList(); + QTest::newRow("fuzzy5") << fuzzyItems << QStringLiteral("foo/bar") << StringList({ + QStringLiteral("/foo/bar.h"), + QStringLiteral("/foo/fooXbar.h"), + QStringLiteral("/foo/fXoXoXbXaXr.h") + }); + QTest::newRow("fuzzy6") << fuzzyItems << QStringLiteral("foobar") << StringList({ + QStringLiteral("/foo/fooXbar.h"), + QStringLiteral("/foo/fXoXoXbXaXr.h"), + QStringLiteral("/bar/FOOxBAR.h") + }); + } } void TestQuickOpen::testProjectFileFilter() { QTemporaryDir dir; TestProject* project = new TestProject(Path(dir.path())); ProjectFolderItem* foo = createChild(project->projectItem(), QStringLiteral("foo")); createChild(foo, QStringLiteral("bar")); createChild(foo, QStringLiteral("asdf")); createChild(foo, QStringLiteral("space bar")); ProjectFolderItem* asdf = createChild(project->projectItem(), QStringLiteral("asdf")); createChild(asdf, QStringLiteral("bar")); QTemporaryFile tmpFile; tmpFile.setFileName(dir.path() + "/aaaa"); QVERIFY(tmpFile.open()); ProjectFileItem* aaaa = new ProjectFileItem(QStringLiteral("aaaa"), project->projectItem()); QCOMPARE(project->fileSet().size(), 5); ProjectFileDataProvider provider; QCOMPARE(provider.itemCount(), 0u); projectController->addProject(project); const QStringList original = QStringList() << QStringLiteral("aaaa") << QStringLiteral("asdf/bar") << QStringLiteral("foo/asdf") << QStringLiteral("foo/bar") << QStringLiteral("foo/space bar"); // lazy load QCOMPARE(provider.itemCount(), 0u); provider.reset(); QCOMPARE(items(provider), original); QCOMPARE(provider.itemPath(provider.items().first()), aaaa->path()); QCOMPARE(provider.data(0)->text(), QStringLiteral("aaaa")); // don't show opened file QVERIFY(core->documentController()->openDocument(QUrl::fromLocalFile(tmpFile.fileName()))); // lazy load again QCOMPARE(items(provider), original); provider.reset(); QCOMPARE(items(provider), QStringList() << QStringLiteral("asdf/bar") << QStringLiteral("foo/asdf") << QStringLiteral("foo/bar") << QStringLiteral("foo/space bar")); // prefer files starting with filter provider.setFilterText(QStringLiteral("as")); qDebug() << items(provider); QCOMPARE(items(provider), QStringList() << QStringLiteral("foo/asdf") << QStringLiteral("asdf/bar")); // clear filter provider.reset(); QCOMPARE(items(provider), QStringList() << QStringLiteral("asdf/bar") << QStringLiteral("foo/asdf") << QStringLiteral("foo/bar") << QStringLiteral("foo/space bar")); // update on document close, lazy load again core->documentController()->closeAllDocuments(); QCOMPARE(items(provider), QStringList() << QStringLiteral("asdf/bar") << QStringLiteral("foo/asdf") << QStringLiteral("foo/bar") << QStringLiteral("foo/space bar")); provider.reset(); QCOMPARE(items(provider), original); ProjectFileItem* blub = createChild(project->projectItem(), QStringLiteral("blub")); // lazy load QCOMPARE(provider.itemCount(), 5u); provider.reset(); QCOMPARE(provider.itemCount(), 6u); // ensure we don't add stuff multiple times QMetaObject::invokeMethod(&provider, "fileAddedToSet", Q_ARG(KDevelop::IProject*, project), Q_ARG(KDevelop::IndexedString, blub->indexedPath())); QCOMPARE(provider.itemCount(), 6u); provider.reset(); QCOMPARE(provider.itemCount(), 6u); // lazy load in this implementation delete blub; QCOMPARE(provider.itemCount(), 6u); provider.reset(); QCOMPARE(provider.itemCount(), 5u); QCOMPARE(items(provider), original); // allow filtering by path to project provider.setFilterText(dir.path()); QCOMPARE(items(provider), original); Path buildFolderItem(project->path().parent(), QStringLiteral(".build/generated.h")); new ProjectFileItem(project, buildFolderItem, project->projectItem()); // lazy load QCOMPARE(items(provider), original); provider.reset(); QCOMPARE(items(provider), QStringList() << QStringLiteral("aaaa") << QStringLiteral("asdf/bar") << QStringLiteral("foo/asdf") << QStringLiteral("foo/bar") << QStringLiteral("foo/space bar") << QStringLiteral("../.build/generated.h")); projectController->closeProject(project); provider.reset(); QVERIFY(!provider.itemCount()); }