diff --git a/kdevplatform/project/tests/test_projectmodel.cpp b/kdevplatform/project/tests/test_projectmodel.cpp index 534540b72e..234ea6e7dc 100644 --- a/kdevplatform/project/tests/test_projectmodel.cpp +++ b/kdevplatform/project/tests/test_projectmodel.cpp @@ -1,546 +1,560 @@ /*************************************************************************** * Copyright 2010 Andreas Pakulat * * * * 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 Library 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. * ***************************************************************************/ #include "test_projectmodel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; void debugItemModel(QAbstractItemModel* m, const QModelIndex& parent=QModelIndex(), int depth=0) { Q_ASSERT(m); qDebug() << QByteArray(depth*2, '-') << m->data(parent).toString(); for(int i=0; irowCount(parent); i++) { debugItemModel(m, m->index(i, 0, parent), depth+1); } } void TestProjectModel::initTestCase() { AutoTestShell::init(); TestCore::initialize(Core::NoUi); qRegisterMetaType("QModelIndex"); model = ICore::self()->projectController()->projectModel(); new ModelTest( model, this ); proxy = new ProjectProxyModel( model ); new ModelTest(proxy, proxy); proxy->setSourceModel(model); } void TestProjectModel::init() { model->clear(); } void TestProjectModel::cleanupTestCase() { TestCore::shutdown(); } void TestProjectModel::testCreateFileSystemItems() { QFETCH( int, itemType ); QFETCH( Path, itemPath ); QFETCH( Path, expectedItemPath ); QFETCH( QString, expectedItemText ); QFETCH( QStringList, expectedRelativeItemPath ); QFETCH( int, expectedItemRow ); ProjectBaseItem* newitem = nullptr; switch( itemType ) { case ProjectBaseItem::Folder: newitem = new ProjectFolderItem( nullptr, itemPath ); break; case ProjectBaseItem::BuildFolder: newitem = new ProjectBuildFolderItem( nullptr, itemPath ); break; case ProjectBaseItem::File: newitem = new ProjectFileItem( nullptr, itemPath ); break; } int origRowCount = model->rowCount(); model->appendRow( newitem ); QCOMPARE( model->rowCount(), origRowCount+1 ); QCOMPARE( newitem->row(), expectedItemRow ); QModelIndex idx = model->index( expectedItemRow, 0, QModelIndex() ); QVERIFY( model->itemFromIndex( idx ) ); QCOMPARE( model->itemFromIndex( idx ), newitem ); QCOMPARE( newitem->text(), expectedItemText ); QCOMPARE( newitem->path(), expectedItemPath ); if( itemType == ProjectBaseItem::File ) { QCOMPARE( dynamic_cast( newitem )->fileName(), expectedItemText ); } if( itemType == ProjectBaseItem::Folder || itemType == ProjectBaseItem::BuildFolder ) { QCOMPARE( dynamic_cast( newitem )->folderName(), expectedItemText ); } QCOMPARE( newitem->type(), itemType ); QCOMPARE( model->data( idx ).toString(), expectedItemText ); QCOMPARE( model->indexFromItem( newitem ), idx ); QCOMPARE( model->pathFromIndex( idx ), expectedRelativeItemPath ); QCOMPARE( model->pathToIndex( expectedRelativeItemPath ), idx ); } void TestProjectModel::testCreateFileSystemItems_data() { + QString testRootDir = QDir::rootPath() + QStringLiteral("rootdir"); + QString testRootFile = QDir::rootPath() + QStringLiteral("rootfile"); QTest::addColumn( "itemType" ); QTest::addColumn( "itemPath" ); QTest::addColumn( "expectedItemPath" ); QTest::addColumn( "expectedItemText" ); QTest::addColumn( "expectedRelativeItemPath" ); QTest::addColumn( "expectedItemRow" ); QTest::newRow("RootFolder") << (int)ProjectBaseItem::Folder - << Path(QUrl::fromLocalFile(QStringLiteral("/rootdir"))) - << Path(QUrl::fromLocalFile(QStringLiteral("/rootdir/"))) + << Path(QUrl::fromLocalFile(testRootDir)) + << Path(QUrl::fromLocalFile(testRootDir)) << QStringLiteral("rootdir") << ( QStringList() << QStringLiteral("rootdir") ) << 0; QTest::newRow("RootBuildFolder") << (int)ProjectBaseItem::BuildFolder - << Path(QUrl::fromLocalFile(QStringLiteral("/rootdir"))) - << Path(QUrl::fromLocalFile(QStringLiteral("/rootdir/"))) + << Path(QUrl::fromLocalFile(testRootDir)) + << Path(QUrl::fromLocalFile(testRootDir)) << QStringLiteral("rootdir") << ( QStringList() << QStringLiteral("rootdir") ) << 0; QTest::newRow("RootFile") << (int)ProjectBaseItem::File - << Path(QUrl::fromLocalFile(QStringLiteral("/rootfile"))) - << Path(QUrl::fromLocalFile(QStringLiteral("/rootfile"))) + << Path(QUrl::fromLocalFile(testRootFile)) + << Path(QUrl::fromLocalFile(testRootFile)) << QStringLiteral("rootfile") << ( QStringList() << QStringLiteral("rootfile") ) << 0; } void TestProjectModel::testCreateTargetItems() { QFETCH( int, itemType ); QFETCH( QString, itemText ); QFETCH( QString, expectedItemText ); QFETCH( QStringList, expectedItemPath ); QFETCH( int, expectedItemRow ); ProjectBaseItem* newitem = nullptr; switch( itemType ) { case ProjectBaseItem::Target: newitem = new ProjectTargetItem( nullptr, itemText ); break; case ProjectBaseItem::LibraryTarget: newitem = new ProjectLibraryTargetItem( nullptr, itemText ); break; } int origRowCount = model->rowCount(); model->appendRow( newitem ); QCOMPARE( model->rowCount(), origRowCount+1 ); QCOMPARE( newitem->row(), expectedItemRow ); QModelIndex idx = model->index( expectedItemRow, 0, QModelIndex() ); QVERIFY( model->itemFromIndex( idx ) ); QCOMPARE( model->itemFromIndex( idx ), newitem ); QCOMPARE( newitem->text(), expectedItemText ); QCOMPARE( newitem->type(), itemType ); QCOMPARE( model->data( idx ).toString(), expectedItemText ); QCOMPARE( model->indexFromItem( newitem ), idx ); QCOMPARE( model->pathFromIndex( idx ), expectedItemPath ); QCOMPARE( model->pathToIndex( expectedItemPath ), idx ); } void TestProjectModel::testCreateTargetItems_data() { QTest::addColumn( "itemType" ); QTest::addColumn( "itemText" ); QTest::addColumn( "expectedItemText" ); QTest::addColumn( "expectedItemPath" ); QTest::addColumn( "expectedItemRow" ); QTest::newRow("RootTarget") << (int)ProjectBaseItem::Target << "target" << QStringLiteral("target") << ( QStringList() << QStringLiteral("target") ) << 0; QTest::newRow("RootLibraryTarget") << (int)ProjectBaseItem::LibraryTarget << "libtarget" << QStringLiteral("libtarget") << ( QStringList() << QStringLiteral("libtarget") ) << 0; } void TestProjectModel::testChangeWithProxyModel() { + QString projectFolderPath = QDir::rootPath() + QStringLiteral("folder1"); + QString projectFilePath = QDir::rootPath() + QStringLiteral("folder1/file1"); QSortFilterProxyModel* proxy = new QSortFilterProxyModel( this ); proxy->setSourceModel( model ); - ProjectFolderItem* root = new ProjectFolderItem( nullptr, Path(QUrl::fromLocalFile(QStringLiteral("/folder1"))) ); - root->appendRow( new ProjectFileItem( nullptr, Path(QUrl::fromLocalFile(QStringLiteral("/folder1/file1"))) ) ); + ProjectFolderItem* root = new ProjectFolderItem( nullptr, Path(QUrl::fromLocalFile(projectFolderPath)) ); + root->appendRow( new ProjectFileItem( nullptr, Path(QUrl::fromLocalFile(projectFilePath)) ) ); model->appendRow( root ); QCOMPARE( model->rowCount(), 1 ); QCOMPARE( proxy->rowCount(), 1 ); model->removeRow( 0 ); QCOMPARE( model->rowCount(), 0 ); QCOMPARE( proxy->rowCount(), 0 ); } void TestProjectModel::testCreateSimpleHierarchy() { QString folderName = QStringLiteral("rootfolder"); QString fileName = QStringLiteral("file"); QString targetName = QStringLiteral("testtarged"); QString cppFileName = QStringLiteral("file.cpp"); - ProjectFolderItem* rootFolder = new ProjectFolderItem( nullptr, Path(QUrl::fromLocalFile("/"+folderName)) ); + ProjectFolderItem* rootFolder = new ProjectFolderItem( nullptr, Path(QUrl::fromLocalFile( QDir::rootPath() + folderName )) ); QCOMPARE(rootFolder->baseName(), folderName); ProjectFileItem* file = new ProjectFileItem( fileName, rootFolder ); QCOMPARE(file->baseName(), fileName); ProjectTargetItem* target = new ProjectTargetItem( nullptr, targetName ); rootFolder->appendRow( target ); ProjectFileItem* targetfile = new ProjectFileItem( nullptr, Path(rootFolder->path(), cppFileName), target ); model->appendRow( rootFolder ); QCOMPARE( model->rowCount(), 1 ); QModelIndex folderIdx = model->index( 0, 0, QModelIndex() ); QCOMPARE( model->data( folderIdx ).toString(), folderName ); QCOMPARE( model->rowCount( folderIdx ), 2 ); QCOMPARE( model->itemFromIndex( folderIdx ), rootFolder ); QVERIFY( rootFolder->hasFileOrFolder( fileName ) ); QModelIndex fileIdx = model->index( 0, 0, folderIdx ); QCOMPARE( model->data( fileIdx ).toString(), fileName ); QCOMPARE( model->rowCount( fileIdx ), 0 ); QCOMPARE( model->itemFromIndex( fileIdx ), file ); QModelIndex targetIdx = model->index( 1, 0, folderIdx ); QCOMPARE( model->data( targetIdx ).toString(), targetName ); QCOMPARE( model->rowCount( targetIdx ), 1 ); QCOMPARE( model->itemFromIndex( targetIdx ), target ); QModelIndex targetFileIdx = model->index( 0, 0, targetIdx ); QCOMPARE( model->data( targetFileIdx ).toString(), cppFileName ); QCOMPARE( model->rowCount( targetFileIdx ), 0 ); QCOMPARE( model->itemFromIndex( targetFileIdx ), targetfile ); rootFolder->removeRow( 1 ); QCOMPARE( model->rowCount( folderIdx ), 1 ); delete file; file = nullptr; // Check that we also find a folder with the fileName new ProjectFolderItem( fileName, rootFolder ); QVERIFY( rootFolder->hasFileOrFolder( fileName ) ); delete rootFolder; QCOMPARE( model->rowCount(), 0 ); } void TestProjectModel::testItemSanity() { +#ifdef Q_OS_WIN + QString child3Path = QStringLiteral("file:///c:/bcd"); + QString child4Path = QStringLiteral("file:///c:/abcd"); +#else + QString child3Path = QStringLiteral("file:///bcd"); + QString child4Path = QStringLiteral("file:///abcd"); +#endif + QString newtestPath = QDir::rootPath() + QStringLiteral("newtest"); ProjectBaseItem* parent = new ProjectBaseItem( nullptr, QStringLiteral("test") ); ProjectBaseItem* child = new ProjectBaseItem( nullptr, QStringLiteral("test"), parent ); ProjectBaseItem* child2 = new ProjectBaseItem( nullptr, QStringLiteral("ztest"), parent ); - ProjectFileItem* child3 = new ProjectFileItem( nullptr, Path(QUrl(QStringLiteral("file:///bcd"))), parent ); - ProjectFileItem* child4 = new ProjectFileItem( nullptr, Path(QUrl(QStringLiteral("file:///abcd"))), parent ); + ProjectFileItem* child3 = new ProjectFileItem( nullptr, Path(QUrl(child3Path)), parent ); + ProjectFileItem* child4 = new ProjectFileItem( nullptr, Path(QUrl(child4Path)), parent ); // Just some basic santiy checks on the API QCOMPARE( parent->child( 0 ), child ); QCOMPARE( parent->row(), -1 ); QVERIFY( !parent->child( -1 ) ); QVERIFY( !parent->file() ); QVERIFY( !parent->folder() ); QVERIFY( !parent->project() ); QVERIFY( !parent->child( parent->rowCount() ) ); QCOMPARE( parent->iconName(), QString() ); QCOMPARE( parent->index(), QModelIndex() ); QCOMPARE( child->type(), (int)ProjectBaseItem::BaseItem ); QCOMPARE( child->lessThan( child2 ), true ); QCOMPARE( child3->lessThan( child4 ), false ); // Check that model is properly emitting data-changes model->appendRow( parent ); QCOMPARE( parent->index(), model->index(0, 0, QModelIndex()) ); QSignalSpy s( model, SIGNAL(dataChanged(QModelIndex,QModelIndex)) ); - parent->setPath( Path(QStringLiteral("/newtest")) ); + parent->setPath( Path(newtestPath) ); QCOMPARE( s.count(), 1 ); QCOMPARE( model->data( parent->index() ).toString(), QStringLiteral("newtest") ); parent->removeRow( child->row() ); } void TestProjectModel::testTakeRow() { QScopedPointer parent(new ProjectBaseItem( nullptr, QStringLiteral("test") )); QScopedPointer child(new ProjectBaseItem( nullptr, QStringLiteral("test"), parent.data() )); QScopedPointer subchild(new ProjectBaseItem( nullptr, QStringLiteral("subtest"), child.data() )); model->appendRow( parent.data() ); QCOMPARE( parent->model(), model ); QCOMPARE( child->model(), model ); QCOMPARE( subchild->model(), model ); parent->takeRow( child->row() ); QCOMPARE( child->model(), static_cast(nullptr) ); QCOMPARE( subchild->model(), static_cast(nullptr) ); } void TestProjectModel::testRename() { + QString projectFolderPath = QDir::rootPath() + QStringLiteral("dummyprojectfolder"); QFETCH( int, itemType ); QFETCH( QString, itemText ); QFETCH( QString, newName ); QFETCH( bool, datachangesignal ); QFETCH( QString, expectedItemText ); QFETCH( int, expectedRenameCode ); - const Path projectFolder = Path(QUrl::fromLocalFile(QStringLiteral("/dummyprojectfolder"))); + const Path projectFolder = Path(QUrl::fromLocalFile(projectFolderPath)); QScopedPointer proj(new TestProject()); ProjectFolderItem* rootItem = new ProjectFolderItem( proj.data(), projectFolder, nullptr); proj->setProjectItem( rootItem ); new ProjectFileItem(QStringLiteral("existing"), rootItem); ProjectBaseItem* item = nullptr; if( itemType == ProjectBaseItem::Target ) { item = new ProjectTargetItem( proj.data(), itemText, rootItem ); } else if( itemType == ProjectBaseItem::File ) { item = new ProjectFileItem( itemText, rootItem ); } else if( itemType == ProjectBaseItem::Folder ) { item = new ProjectFolderItem( itemText, rootItem ); } else if( itemType == ProjectBaseItem::BuildFolder ) { item = new ProjectBuildFolderItem( itemText, rootItem ); } Q_ASSERT( item ); QCOMPARE(item->model(), model); QSignalSpy s( model, SIGNAL(dataChanged(QModelIndex,QModelIndex)) ); ProjectBaseItem::RenameStatus stat = item->rename( newName ); QCOMPARE( (int)stat, expectedRenameCode ); if( datachangesignal ) { QCOMPARE( s.count(), 1 ); QCOMPARE( qvariant_cast( s.takeFirst().at(0) ), item->index() ); } else { QCOMPARE( s.count(), 0 ); } QCOMPARE( item->text(), expectedItemText ); } void TestProjectModel::testRename_data() { QTest::addColumn( "itemType" ); QTest::addColumn( "itemText" ); QTest::addColumn( "newName" ); QTest::addColumn( "datachangesignal" ); QTest::addColumn( "expectedItemText" ); QTest::addColumn( "expectedRenameCode" ); QTest::newRow("RenameableTarget") << (int)ProjectBaseItem::Target << QStringLiteral("target") << QStringLiteral("othertarget") << true << QStringLiteral("othertarget") << (int)ProjectBaseItem::RenameOk; QTest::newRow("RenameableFile") << (int)ProjectBaseItem::File << QStringLiteral("newfile.cpp") << QStringLiteral("otherfile.cpp") << true << QStringLiteral("otherfile.cpp") << (int)ProjectBaseItem::RenameOk; QTest::newRow("SourceAndDestinationFileEqual") << (int)ProjectBaseItem::File << QStringLiteral("newfile.cpp") << QStringLiteral("newfile.cpp") << false << QStringLiteral("newfile.cpp") << (int)ProjectBaseItem::RenameOk; QTest::newRow("RenameableFolder") << (int)ProjectBaseItem::Folder << QStringLiteral("newfolder") << QStringLiteral("otherfolder") << true << QStringLiteral("otherfolder") << (int)ProjectBaseItem::RenameOk; QTest::newRow("SourceAndDestinationFolderEqual") << (int)ProjectBaseItem::Folder << QStringLiteral("newfolder") << QStringLiteral("newfolder") << false << QStringLiteral("newfolder") << (int)ProjectBaseItem::RenameOk; QTest::newRow("RenameableBuildFolder") << (int)ProjectBaseItem::BuildFolder << QStringLiteral("newbfolder") << QStringLiteral("otherbfolder") << true << QStringLiteral("otherbfolder") << (int)ProjectBaseItem::RenameOk; QTest::newRow("SourceAndDestinationBuildFolderEqual") << (int)ProjectBaseItem::BuildFolder << QStringLiteral("newbfolder") << QStringLiteral("newbfolder") << false << QStringLiteral("newbfolder") << (int)ProjectBaseItem::RenameOk; QTest::newRow("ExistingFileError") << (int)ProjectBaseItem::Folder << QStringLiteral("mynew") << QStringLiteral("existing") << false << QStringLiteral("mynew") << (int)ProjectBaseItem::ExistingItemSameName; QTest::newRow("InvalidNameError") << (int)ProjectBaseItem::File << QStringLiteral("mynew") << QStringLiteral("other/bash") << false << QStringLiteral("mynew") << (int)ProjectBaseItem::InvalidNewName; } void TestProjectModel::testWithProject() { + QString projectFolderPath = QDir::rootPath() + QStringLiteral("dummyprojectfolder"); QScopedPointer proj(new TestProject()); - ProjectFolderItem* rootItem = new ProjectFolderItem( proj.data(), Path(QUrl::fromLocalFile(QStringLiteral("/dummyprojectfolder"))), nullptr); + ProjectFolderItem* rootItem = new ProjectFolderItem( proj.data(), Path(QUrl::fromLocalFile(projectFolderPath)), nullptr); proj->setProjectItem( rootItem ); ProjectBaseItem* item = model->itemFromIndex( model->index( 0, 0 ) ); QCOMPARE( item, rootItem ); QCOMPARE( item->text(), proj->name() ); QCOMPARE( item->path(), proj->path() ); } void TestProjectModel::testItemsForPath() { QFETCH(Path, path); QFETCH(ProjectBaseItem*, root); QFETCH(int, matches); model->appendRow(root); auto items = model->itemsForPath(IndexedString(path.pathOrUrl())); QCOMPARE(items.size(), matches); foreach(ProjectBaseItem* item, items) { QVERIFY(item->path() == path); } model->clear(); } void TestProjectModel::testItemsForPath_data() { QTest::addColumn("path"); QTest::addColumn("root"); QTest::addColumn("matches"); { ProjectFolderItem* root = new ProjectFolderItem(nullptr, Path(QUrl::fromLocalFile(QDir::tempPath()))); ProjectFileItem* file = new ProjectFileItem(QStringLiteral("a"), root); QTest::newRow("find one") << file->path() << static_cast(root) << 1; } { ProjectFolderItem* root = new ProjectFolderItem(nullptr, Path(QUrl::fromLocalFile(QDir::tempPath()))); ProjectFolderItem* folder = new ProjectFolderItem(QStringLiteral("a"), root); ProjectFileItem* file = new ProjectFileItem(QStringLiteral("foo"), folder); ProjectTargetItem* target = new ProjectTargetItem(nullptr, QStringLiteral("b"), root); ProjectFileItem* file2 = new ProjectFileItem(nullptr, file->path(), target); Q_UNUSED(file2); QTest::newRow("find two") << file->path() << static_cast(root) << 2; } } void TestProjectModel::testProjectProxyModel() { ProjectFolderItem* root = new ProjectFolderItem(nullptr, Path(QUrl::fromLocalFile(QDir::tempPath()))); new ProjectFileItem(QStringLiteral("b1"), root); new ProjectFileItem(QStringLiteral("a1"), root); new ProjectFileItem(QStringLiteral("d1"), root); new ProjectFileItem(QStringLiteral("c1"), root); model->appendRow(root); QModelIndex proxyRoot = proxy->mapFromSource(root->index()); QCOMPARE(model->rowCount(root->index()), 4); QCOMPARE(proxy->rowCount(proxyRoot), 4); QCOMPARE(proxy->index(0, 0, proxy->index(0, 0)).data().toString(), QStringLiteral("a1")); QCOMPARE(proxy->index(1, 0, proxy->index(0, 0)).data().toString(), QStringLiteral("b1")); QCOMPARE(proxy->index(2, 0, proxy->index(0, 0)).data().toString(), QStringLiteral("c1")); QCOMPARE(proxy->index(3, 0, proxy->index(0, 0)).data().toString(), QStringLiteral("d1")); model->clear(); } void TestProjectModel::testProjectFileSet() { QScopedPointer project(new TestProject()); QVERIFY(project->fileSet().isEmpty()); Path path(QUrl::fromLocalFile(QDir::tempPath() + "/a")); ProjectFileItem* item = new ProjectFileItem(project.data(), path, project->projectItem()); QCOMPARE(project->fileSet().size(), 1); qDebug() << path << project->fileSet().toList().at(0).toUrl(); QCOMPARE(Path(project->fileSet().toList().at(0).toUrl()), path); delete item; QVERIFY(project->fileSet().isEmpty()); } void TestProjectModel::testProjectFileIcon() { QMimeDatabase db; QScopedPointer item(new ProjectFileItem(nullptr, Path(QDir::tempPath() + "/foo.txt"))); const QString txtIcon = db.mimeTypeForUrl(item->path().toUrl()).iconName(); QCOMPARE(item->iconName(), txtIcon); item->setPath(Path(QDir::tempPath() + "/bar.cpp")); QCOMPARE(item->iconName(), db.mimeTypeForUrl(item->path().toUrl()).iconName()); QVERIFY(item->iconName() != txtIcon); } QTEST_MAIN(TestProjectModel) diff --git a/kdevplatform/util/path.cpp b/kdevplatform/util/path.cpp index 621a41faf2..5ca2a561e8 100644 --- a/kdevplatform/util/path.cpp +++ b/kdevplatform/util/path.cpp @@ -1,511 +1,515 @@ /* * This file is part of KDevelop * Copyright 2012 Milian Wolff * Copyright 2015 Kevin Funk * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 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 6 of version 3 of the license. * * 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "path.h" #include #include #include using namespace KDevelop; namespace { inline bool isWindowsDriveLetter(const QString& segment) { #ifdef Q_OS_WIN return segment.size() == 2 && segment.at(0).isLetter() && segment.at(1) == QLatin1Char(':'); #else Q_UNUSED(segment); return false; #endif } inline bool isAbsolutePath(const QString& path) { if (path.startsWith(QLatin1Char('/'))) { return true; // Even on Windows: Potentially a path of a remote URL } #ifdef Q_OS_WIN return path.size() >= 2 && path.at(0).isLetter() && path.at(1) == QLatin1Char(':'); #else return false; #endif } } QString KDevelop::toUrlOrLocalFile(const QUrl& url, QUrl::FormattingOptions options) { const auto str = url.toString(options | QUrl::PreferLocalFile); #ifdef Q_OS_WIN // potentially strip leading slash if (url.isLocalFile() && !str.isEmpty() && str[0] == QLatin1Char('/')) { return str.mid(1); // expensive copying, but we'd like toString(...) to properly format everything first } #endif return str; } Path::Path() { } Path::Path(const QString& pathOrUrl) : Path(QUrl::fromUserInput(pathOrUrl, QString(), QUrl::DefaultResolution)) { } Path::Path(const QUrl& url) { if (!url.isValid()) { // empty or invalid Path return; } // we do not support urls with: // - fragments // - sub urls // - query // nor do we support relative urls if (url.hasFragment() || url.hasQuery() || url.isRelative() || url.path().isEmpty()) { // invalid qWarning("Path::init: invalid/unsupported Path encountered: \"%s\"", qPrintable(url.toDisplayString(QUrl::PreferLocalFile))); return; } if (!url.isLocalFile()) { // handle remote urls QString urlPrefix = url.scheme() + QLatin1String("://"); const QString user = url.userName(); if (!user.isEmpty()) { urlPrefix += user + QLatin1Char('@'); } urlPrefix += url.host(); if (url.port() != -1) { urlPrefix += QLatin1Char(':') + QString::number(url.port()); } m_data << urlPrefix; } addPath(url.isLocalFile() ? url.toLocalFile() : url.path()); // support for root paths, they are valid but don't really contain any data if (m_data.isEmpty() || (isRemote() && m_data.size() == 1)) { m_data << QString(); } } Path::Path(const Path& other, const QString& child) : m_data(other.m_data) { if (isAbsolutePath(child)) { // absolute path: only share the remote part of @p other m_data.resize(isRemote() ? 1 : 0); } else if (!other.isValid() && !child.isEmpty()) { qWarning("Path::Path: tried to append relative path \"%s\" to invalid base", qPrintable(child)); return; } addPath(child); } static QString generatePathOrUrl(bool onlyPath, bool isLocalFile, const QVector& data) { // more or less a copy of QtPrivate::QStringList_join const int size = data.size(); if (size == 0) { return QString(); } int totalLength = 0; // separators: '/' totalLength += size; // skip Path segment if we only want the path int start = (onlyPath && !isLocalFile) ? 1 : 0; // path and url prefix for (int i = start; i < size; ++i) { totalLength += data.at(i).size(); } // build string representation QString res; res.reserve(totalLength); #ifdef Q_OS_WIN if (start == 0 && isLocalFile) { + if(!data.at(0).endsWith(QLatin1Char(':'))) { + qWarning("Path::generatePathOrUrl: invalid Windows drive encountered (expected C: or similar): \"%s\"", + qPrintable(data.at(0))); + } Q_ASSERT(data.at(0).endsWith(QLatin1Char(':'))); // assume something along "C:" res += data.at(0); start++; } #endif for (int i = start; i < size; ++i) { if (i || isLocalFile) { res += QLatin1Char('/'); } res += data.at(i); } return res; } QString Path::pathOrUrl() const { return generatePathOrUrl(false, isLocalFile(), m_data); } QString Path::path() const { return generatePathOrUrl(true, isLocalFile(), m_data); } QString Path::toLocalFile() const { return isLocalFile() ? path() : QString(); } QString Path::relativePath(const Path& path) const { if (!path.isValid()) { return QString(); } if (!isValid() || remotePrefix() != path.remotePrefix()) { // different remote destinations or we are invalid, return input as-is return path.pathOrUrl(); } // while I'd love to use QUrl::relativePath here, it seems to behave pretty // strangely, and adds unexpected "./" at the start for example // so instead, do it on our own based on _relativePath in kurl.cpp // this should also be more performant I think // Find where they meet int level = isRemote() ? 1 : 0; const int maxLevel = qMin(m_data.count(), path.m_data.count()); while(level < maxLevel && m_data.at(level) == path.m_data.at(level)) { ++level; } // Need to go down out of our path to the common branch. // but keep in mind that e.g. '/' paths have an empty name int backwardSegments = m_data.count() - level; if (backwardSegments && level < maxLevel && m_data.at(level).isEmpty()) { --backwardSegments; } // Now up up from the common branch to the second path. int forwardSegmentsLength = 0; for (int i = level; i < path.m_data.count(); ++i) { forwardSegmentsLength += path.m_data.at(i).length(); // slashes if (i + 1 != path.m_data.count()) { forwardSegmentsLength += 1; } } QString relativePath; relativePath.reserve((backwardSegments * 3) + forwardSegmentsLength); for(int i = 0; i < backwardSegments; ++i) { relativePath.append(QLatin1String("../")); } for (int i = level; i < path.m_data.count(); ++i) { relativePath.append(path.m_data.at(i)); if (i + 1 != path.m_data.count()) { relativePath.append(QLatin1Char('/')); } } Q_ASSERT(relativePath.length() == ((backwardSegments * 3) + forwardSegmentsLength)); return relativePath; } static bool isParentPath(const QVector& parent, const QVector& child, bool direct) { if (direct && child.size() != parent.size() + 1) { return false; } else if (!direct && child.size() <= parent.size()) { return false; } for (int i = 0; i < parent.size(); ++i) { if (child.at(i) != parent.at(i)) { // support for trailing '/' if (i + 1 == parent.size() && parent.at(i).isEmpty()) { return true; } // otherwise we take a different branch here return false; } } return true; } bool Path::isParentOf(const Path& path) const { if (!isValid() || !path.isValid() || remotePrefix() != path.remotePrefix()) { return false; } return isParentPath(m_data, path.m_data, false); } bool Path::isDirectParentOf(const Path& path) const { if (!isValid() || !path.isValid() || remotePrefix() != path.remotePrefix()) { return false; } return isParentPath(m_data, path.m_data, true); } QString Path::remotePrefix() const { return isRemote() ? m_data.first() : QString(); } bool Path::operator<(const Path& other) const { const int size = m_data.size(); const int otherSize = other.m_data.size(); const int toCompare = qMin(size, otherSize); // compare each Path segment in turn and try to return early for (int i = 0; i < toCompare; ++i) { int comparison = m_data.at(i).compare(other.m_data.at(i)); if (comparison == 0) { // equal, try next segment continue; } else { // return whether our segment is less then the other one return comparison < 0; } } // when we reach this point, all elements that we compared where equal // thus return whether we have less items than the other Path return size < otherSize; } QUrl Path::toUrl() const { return QUrl::fromUserInput(pathOrUrl()); } bool Path::isLocalFile() const { // if the first data element contains a '/' it is a Path prefix return !m_data.isEmpty() && !m_data.first().contains(QLatin1Char('/')); } bool Path::isRemote() const { return !m_data.isEmpty() && m_data.first().contains(QLatin1Char('/')); } QString Path::lastPathSegment() const { // remote Paths are offset by one, thus never return the first item of them as file name if (m_data.isEmpty() || (!isLocalFile() && m_data.size() == 1)) { return QString(); } return m_data.last(); } void Path::setLastPathSegment(const QString& name) { // remote Paths are offset by one, thus never return the first item of them as file name if (m_data.isEmpty() || (!isLocalFile() && m_data.size() == 1)) { // append the name to empty Paths or remote Paths only containing the Path prefix m_data.append(name); } else { // overwrite the last data member m_data.last() = name; } } static void cleanPath(QVector* data, const bool isRemote) { if (data->isEmpty()) { return; } const int startOffset = isRemote ? 1 : 0; const auto start = data->begin() + startOffset; auto it = start; while(it != data->end()) { if (*it == QLatin1String("..")) { if (it == start) { it = data->erase(it); } else { if (isWindowsDriveLetter(*(it - 1))) { it = data->erase(it); // keep the drive letter } else { it = data->erase(it - 1, it + 1); } } } else if (*it == QLatin1String(".")) { it = data->erase(it); } else { ++it; } } if (data->count() == startOffset) { data->append(QString()); } } // Optimized QString::split code for the specific Path use-case static QVarLengthArray splitPath(const QString &source) { QVarLengthArray list; int start = 0; int end = 0; while ((end = source.indexOf(QLatin1Char('/'), start)) != -1) { if (start != end) { list.append(source.mid(start, end - start)); } start = end + 1; } if (start != source.size()) { list.append(source.mid(start, -1)); } return list; } void Path::addPath(const QString& path) { if (path.isEmpty()) { return; } const auto& newData = splitPath(path); if (newData.isEmpty()) { if (m_data.size() == (isRemote() ? 1 : 0)) { // this represents the root path, we just turned an invalid path into it m_data << QString(); } return; } auto it = newData.begin(); if (!m_data.isEmpty() && m_data.last().isEmpty()) { // the root item is empty, set its contents and continue appending m_data.last() = *it; ++it; } std::copy(it, newData.end(), std::back_inserter(m_data)); cleanPath(&m_data, isRemote()); } Path Path::parent() const { if (m_data.isEmpty()) { return Path(); } Path ret(*this); if (m_data.size() == (1 + (isRemote() ? 1 : 0))) { // keep the root item, but clear it, otherwise we'd make the path invalid // or a URL a local path auto& root = ret.m_data.last(); if (!isWindowsDriveLetter(root)) { root.clear(); } } else { ret.m_data.pop_back(); } return ret; } bool Path::hasParent() const { const int rootIdx = isRemote() ? 1 : 0; return m_data.size() > rootIdx && !m_data[rootIdx].isEmpty(); } void Path::clear() { m_data.clear(); } Path Path::cd(const QString& dir) const { if (!isValid()) { return Path(); } return Path(*this, dir); } namespace KDevelop { uint qHash(const Path& path) { KDevHash hash; foreach (const QString& segment, path.segments()) { hash << qHash(segment); } return hash; } template static Path::List toPathList_impl(const Container& list) { Path::List ret; ret.reserve(list.size()); for (const auto& entry : list) { Path path(entry); if (path.isValid()) { ret << path; } } ret.squeeze(); return ret; } Path::List toPathList(const QList& list) { return toPathList_impl(list); } Path::List toPathList(const QList< QString >& list) { return toPathList_impl(list); } } QDebug operator<<(QDebug s, const Path& string) { s.nospace() << string.pathOrUrl(); return s.space(); } namespace QTest { template<> char *toString(const Path &path) { return qstrdup(qPrintable(path.pathOrUrl())); } }