diff --git a/kdevplatform/language/interfaces/abbreviations.cpp b/kdevplatform/language/interfaces/abbreviations.cpp index 709ea5fccb..86de80d9f4 100644 --- a/kdevplatform/language/interfaces/abbreviations.cpp +++ b/kdevplatform/language/interfaces/abbreviations.cpp @@ -1,232 +1,238 @@ /* 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 { // Taken and adapted for kdevelop from katecompletionmodel.cpp static bool matchesAbbreviationHelper(const QStringRef &word, const QString &typed, const QVarLengthArray< int, 32 > &offsets, int &depth, int atWord = -1, int i = 0) { 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(); } -int matchPathFilter(const Path &toFilter, const QStringList &text) +int matchPathFilter(const Path &toFilter, const QStringList &text, const Path &prefixPath) { enum PathFilterMatchQuality { NoMatch = -1, ExactMatch = 0, StartMatch = 1, OtherMatch = 2 // and anything higher than that }; const QVector& segments = toFilter.segments(); if (text.count() > segments.count()) { // number of segments mismatches, thus item cannot match return NoMatch; } bool allMatched = true; int searchIndex = text.size() - 1; int pathIndex = segments.size() - 1; int lastMatchIndex = -1; // stop early if more search fragments remain than available after path index while (pathIndex >= 0 && searchIndex >= 0 && (pathIndex + text.size() - searchIndex - 1) < segments.size() ) { const QString& segment = segments.at(pathIndex); const QString& typedSegment = text.at(searchIndex); const int matchIndex = segment.indexOf(typedSegment, 0, Qt::CaseInsensitive); const bool isLastPathSegment = pathIndex == segments.size() - 1; const bool isLastSearchSegment = searchIndex == text.size() - 1; // check for exact matches allMatched &= matchIndex == 0 && segment.size() == typedSegment.size(); // check for fuzzy matches bool isMatch = matchIndex != -1; // do fuzzy path matching on the last segment if (!isMatch && isLastPathSegment && isLastSearchSegment) { isMatch = matchesPath(segment, typedSegment); } else if (!isMatch) { // check other segments for abbreviations isMatch = matchesAbbreviation(segment.midRef(0), typedSegment); } if (!isMatch) { // no match, try with next path segment --pathIndex; continue; } // else we matched if (isLastPathSegment) { lastMatchIndex = matchIndex; } --searchIndex; --pathIndex; } if (searchIndex != -1) { return NoMatch; } - // prefer matches whose last element starts with the filter - if (allMatched) { - return ExactMatch; + const int segmentMatchDistance = segments.size() - (pathIndex + 1); + const bool inPrefixPath = segmentMatchDistance > (segments.size() - prefixPath.segments().size()) + && prefixPath.isParentOf(toFilter); + // penalize matches that fall into the shared suffix + const int penalty = (inPrefixPath ) ? 1024 : 0; + + if (allMatched && !inPrefixPath) { + return ExactMatch + penalty; } else if (lastMatchIndex == 0) { - return StartMatch; + // prefer matches whose last element starts with the filter + return StartMatch + penalty; } else { // prefer matches closer to the end of the path - return OtherMatch + segments.size() - pathIndex; + return OtherMatch + segmentMatchDistance + penalty; } } } // namespace KDevelop // kate: space-indent on; indent-width 2 diff --git a/kdevplatform/language/interfaces/abbreviations.h b/kdevplatform/language/interfaces/abbreviations.h index a72b802a08..f5ecf38475 100644 --- a/kdevplatform/language/interfaces/abbreviations.h +++ b/kdevplatform/language/interfaces/abbreviations.h @@ -1,59 +1,59 @@ /* 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; 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); /** * @brief Matches a path against a list of search fragments. * @return -1 when no match is found, otherwise a positive integer, higher values mean lower quality */ -KDEVPLATFORMLANGUAGE_EXPORT int matchPathFilter(const Path& toFilter, const QStringList& text); +KDEVPLATFORMLANGUAGE_EXPORT int matchPathFilter(const Path& toFilter, const QStringList& text, const Path& prefixPath); } #endif // kate: space-indent on; indent-width 2 diff --git a/kdevplatform/language/interfaces/quickopenfilter.h b/kdevplatform/language/interfaces/quickopenfilter.h index 9ea6dd7400..9e9ef1d463 100644 --- a/kdevplatform/language/interfaces/quickopenfilter.h +++ b/kdevplatform/language/interfaces/quickopenfilter.h @@ -1,227 +1,228 @@ /* * 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; } 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; } QVector> matches; for (int i = 0, c = filterBase.size(); i < c; ++i) { const auto& data = filterBase.at(i); - const auto matchQuality = matchPathFilter(static_cast(this)->itemPath(data), text); + const auto matchQuality = matchPathFilter(static_cast(this)->itemPath(data), text, + static_cast(this)->itemPrefixPath(data)); if (matchQuality == -1) { continue; } matches.push_back({matchQuality, i}); } std::sort(matches.begin(), matches.end(), [](const QPair& lhs, const QPair& rhs) { return lhs.first < rhs.first; }); m_filtered.resize(matches.size()); std::transform(matches.begin(), matches.end(), m_filtered.begin(), [&filterBase](const QPair& match) { return filterBase.at(match.second); }); m_oldFilterText = text; } private: QStringList m_oldFilterText; QVector m_filtered; QVector m_items; }; } #endif diff --git a/plugins/quickopen/projectfilequickopen.h b/plugins/quickopen/projectfilequickopen.h index 5ef19a6307..c1dea77784 100644 --- a/plugins/quickopen/projectfilequickopen.h +++ b/plugins/quickopen/projectfilequickopen.h @@ -1,147 +1,152 @@ /* This file is part of the KDE libraries Copyright (C) 2007 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 PROJECT_FILE_QUICKOPEN #define PROJECT_FILE_QUICKOPEN #include #include #include #include namespace KDevelop { class IProject; class ProjectFileItem; } /** * Internal data class for the BaseFileDataProvider and ProjectFileData. */ struct ProjectFile { ProjectFile() : outsideOfProject(false) {} KDevelop::Path path; // project root folder url KDevelop::Path projectPath; // indexed url - only set for project files // currently open documents don't use this! KDevelop::IndexedString indexedPath; // true for files which reside outside of the project root // this happens e.g. for generated files in out-of-source build folders bool outsideOfProject; }; inline bool operator<(const ProjectFile& left, const ProjectFile& right) { if (left.outsideOfProject != right.outsideOfProject) { return !left.outsideOfProject; } return left.path < right.path; } Q_DECLARE_TYPEINFO(ProjectFile, Q_MOVABLE_TYPE); /** * The shared data class that is used by the quick open model. */ class ProjectFileData : public KDevelop::QuickOpenDataBase { public: explicit ProjectFileData(const ProjectFile& file); QString text() const override; QString htmlDescription() const override; bool execute(QString& filterText) override; bool isExpandable() const override; QWidget* expandingWidget() const override; QIcon icon() const override; QList highlighting() const override; QString project() const; KDevelop::Path projectPath() const; private: ProjectFile m_file; }; class BaseFileDataProvider : public KDevelop::QuickOpenDataProviderBase , public KDevelop::PathFilter , public KDevelop::QuickOpenFileSetInterface { Q_OBJECT public: BaseFileDataProvider(); void setFilterText(const QString& text) override; uint itemCount() const override; uint unfilteredItemCount() const override; KDevelop::QuickOpenDataPointer data(uint row) const override; inline KDevelop::Path itemPath(const ProjectFile& data) const { return data.path; } + + inline KDevelop::Path itemPrefixPath(const ProjectFile& data) const + { + return data.projectPath; + } }; /** * QuickOpen data provider for file-completion using project-files. * * It provides all files from all open projects except currently opened ones. */ class ProjectFileDataProvider : public BaseFileDataProvider { Q_OBJECT public: ProjectFileDataProvider(); void reset() override; QSet files() const override; private Q_SLOTS: void projectClosing(KDevelop::IProject*); void projectOpened(KDevelop::IProject*); void fileAddedToSet(KDevelop::ProjectFileItem*); void fileRemovedFromSet(KDevelop::ProjectFileItem*); private: // project files sorted by their url // this is done so we can limit ourselves to a relatively fast // filtering without any expensive sorting in reset(). QVector m_projectFiles; }; /** * Quick open data provider for currently opened documents. */ class OpenFilesDataProvider : public BaseFileDataProvider { Q_OBJECT public: void reset() override; QSet files() const override; }; #endif diff --git a/plugins/quickopen/tests/quickopentestbase.h b/plugins/quickopen/tests/quickopentestbase.h index 4c2e8d782e..0e32d3ca67 100644 --- a/plugins/quickopen/tests/quickopentestbase.h +++ b/plugins/quickopen/tests/quickopentestbase.h @@ -1,85 +1,89 @@ /* * 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 . */ #ifndef KDEVPLATFORM_PLUGIN_QUICKOPENTESTBASE_H #define KDEVPLATFORM_PLUGIN_QUICKOPENTESTBASE_H #include #include #include #include #include #include "../duchainitemquickopen.h" #include "../projectfilequickopen.h" class QuickOpenTestBase : public QObject { Q_OBJECT public: explicit QuickOpenTestBase(KDevelop::TestCore::Setup setup, QObject* parent = nullptr); private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void cleanup(); protected: KDevelop::TestCore* core; KDevelop::TestCore::Setup setup; KDevelop::TestProjectController* projectController; }; class PathTestFilter : public KDevelop::PathFilter { public: KDevelop::Path itemPath(const QString& data) const { return KDevelop::Path(data); } + KDevelop::Path itemPrefixPath(const QString& /*data*/) const + { + return KDevelop::Path(QStringLiteral("/home/user/project")); + } }; KDevelop::TestProject* getProjectWithFiles(int files); template T* createChild(KDevelop::ProjectFolderItem* parent, const QString& childName) { return new T(childName, parent); } QStringList items(const ProjectFileDataProvider& provider); class TestFilter : public KDevelop::Filter { public: QString itemText(const DUChainItem& data) const override { return data.m_text; }; }; Q_DECLARE_METATYPE(QVector) #endif // KDEVPLATFORM_PLUGIN_QUICKOPENTESTBASE_H diff --git a/plugins/quickopen/tests/test_quickopen.cpp b/plugins/quickopen/tests/test_quickopen.cpp index 2501fcce49..e365cb26de 100644 --- a/plugins/quickopen/tests/test_quickopen.cpp +++ b/plugins/quickopen/tests/test_quickopen.cpp @@ -1,343 +1,367 @@ /* * 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") }); } { const StringList a = { QStringLiteral("/home/user/src/code/user/something"), QStringLiteral("/home/user/src/code/home/else"), }; const StringList b = { QStringLiteral("/home/user/src/code/home/else"), QStringLiteral("/home/user/src/code/user/something"), }; QTest::newRow("prefer_multimatch_a_home") << a << QStringLiteral("home") << b; QTest::newRow("prefer_multimatch_b_home") << b << QStringLiteral("home") << b; QTest::newRow("prefer_multimatch_a_user") << a << QStringLiteral("user") << a; QTest::newRow("prefer_multimatch_b_user") << b << QStringLiteral("user") << a; } + { + const StringList a = { + QStringLiteral("/home/user/project/A/file"), + QStringLiteral("/home/user/project/B/project/A/file"), + QStringLiteral("/home/user/project/user/C/D/E"), + }; + const StringList b = { + QStringLiteral("/home/user/project/B/project/A/file"), + QStringLiteral("/home/user/project/A/file"), + }; + const StringList c = { + QStringLiteral("/home/user/project/user/C/D/E"), + QStringLiteral("/home/user/project/A/file"), + QStringLiteral("/home/user/project/B/project/A/file"), + }; + QTest::newRow("prefer_multimatch_a_project/file") << a << QStringLiteral("project/file") << b; + QTest::newRow("prefer_multimatch_b_project/file") << b << QStringLiteral("project/file") << b; + QTest::newRow("prefer_multimatch_a_project/le") << a << QStringLiteral("project/le") << b; + QTest::newRow("prefer_multimatch_b_project/le") << b << QStringLiteral("project/le") << b; + QTest::newRow("prefer_multimatch_a_project/a/file") << a << QStringLiteral("project/a/file") << b; + QTest::newRow("prefer_multimatch_b_project/a/file") << b << QStringLiteral("project/a/file") << b; + QTest::newRow("prefer_multimatch_a_project_user") << a << QStringLiteral("user") << c; + QTest::newRow("prefer_multimatch_c_project_user") << c << QStringLiteral("user") << c; + } } 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()); }