diff --git a/src/plugins/projectexplorer/project.cpp b/src/plugins/projectexplorer/project.cpp index a26dc987490..7bc2c3b636c 100644 --- a/src/plugins/projectexplorer/project.cpp +++ b/src/plugins/projectexplorer/project.cpp @@ -1387,13 +1387,29 @@ const QString TEST_PROJECT_MIMETYPE = "application/vnd.test.qmakeprofile"; const QString TEST_PROJECT_DISPLAYNAME = "testProjectFoo"; const char TEST_PROJECT_ID[] = "Test.Project.Id"; -class TestBuildSystem final : public BuildSystem +class TestBuildSystem : public BuildSystem { public: using BuildSystem::BuildSystem; - void triggerParsing() final {} - QString name() const final { return QLatin1String("test"); } + void triggerParsing() override {} + QString name() const override { return QLatin1String("test"); } + + virtual bool canRenameFile( + Node *context, const FilePath &oldFilePath, const FilePath &newFilePath) override + { + ++canRenameFileCount; + return true; + } + + virtual bool renameFiles( + Node *context, const FilePairs &filesToRename, FilePaths *notRenamed) override + { + ++renameFilesCount; + return true; + } + int canRenameFileCount = 0; + int renameFilesCount = 0; }; class TestProject : public Project @@ -1565,6 +1581,222 @@ void ProjectExplorerTest::testProject_projectTree() QVERIFY(!project.rootProjectNode()); } +class RejectingAllRenameBuildSystem : public TestBuildSystem +{ +public: + using TestBuildSystem::TestBuildSystem; + bool renameFiles(Node *, const FilePairs &, FilePaths *) override + { + ++renameFilesCount; + return false; + } +}; + +class PartiallyRejectingRenameBuildSystem : public TestBuildSystem +{ +public: + using TestBuildSystem::TestBuildSystem; + static FilePaths pathsToReject; + bool renameFiles(Node *, const FilePairs &filePairs, FilePaths *notRenamed) override + { + ++renameFilesCount; + for (auto &[o, _] : filePairs) + if (pathsToReject.contains(o) && notRenamed) + notRenamed->append(o); + return true; + } +}; +FilePaths PartiallyRejectingRenameBuildSystem::pathsToReject; + +class ReparsingBuildSystem : public TestBuildSystem +{ +public: + using TestBuildSystem::TestBuildSystem; + static QPointer projectToReparse; + bool renameFiles(Node *originNode, const FilePairs &filePairs, FilePaths *notRenamed) override + { + const bool ok = TestBuildSystem::renameFiles(originNode, filePairs, notRenamed); + if (notRenamed) { + for (const auto &pair : filePairs) + notRenamed->append(pair.first); + } + if (projectToReparse) { + auto newRoot = std::make_unique(projectToReparse->projectFilePath()); + projectToReparse->setRootProjectNode(std::move(newRoot)); + if (ProjectTree::instance()) + emit ProjectTree::instance()->subtreeChanged(nullptr); + } + return ok; + } +}; +QPointer ReparsingBuildSystem::projectToReparse; + +class RenameTestProject : public Project +{ +public: + explicit RenameTestProject(const TemporaryDirectory &td) + : Project(TEST_PROJECT_MIMETYPE, td.path()) + { + setId(TEST_PROJECT_ID); + setDisplayName(TEST_PROJECT_DISPLAYNAME); + } + + bool needsConfiguration() const final { return false; } + + template + void initialize() + { + setBuildSystemCreator(); + target = addTargetForKit(&kit); + createNodes(); + Q_ASSERT(dynamic_cast(target->buildSystem())); + } + + TestBuildSystem *testBuildSystem() + { + return dynamic_cast(target->buildSystem()); + } + Kit kit; + Target *target = nullptr; + FilePath sourceFile, secondSourceFile; + +private: + void createNodes() + { + sourceFile = projectFilePath().pathAppended("test.cpp"); + secondSourceFile = projectFilePath().pathAppended("test2.cpp"); + QVERIFY(sourceFile.writeFileContents("content1")); + QVERIFY(secondSourceFile.writeFileContents("content2")); + + auto root = std::make_unique(projectFilePath()); + std::vector> vec; + vec.emplace_back(std::make_unique(projectFilePath(), FileType::Project)); + vec.emplace_back(std::make_unique(sourceFile, FileType::Source)); + vec.emplace_back(std::make_unique(secondSourceFile, FileType::Source)); + root->addNestedNodes(std::move(vec)); + setRootProjectNode(std::move(root)); + } +}; + +static FilePath makeRenamedFilePath(const FilePath &original) +{ + return original.chopped(4).stringAppended("_renamed.cpp"); +} + +void ProjectExplorerTest::testProject_renameFile() +{ + TemporaryDirectory tempDir("testProject_renameFile"); + RenameTestProject testProject(tempDir); + testProject.initialize(); + auto sourceFileNode = const_cast(testProject.nodeForFilePath(testProject.sourceFile)); + QVERIFY(sourceFileNode); + const FilePath testRenamed = makeRenamedFilePath(testProject.sourceFile); + QList> nodesToRename{{sourceFileNode, testRenamed}}; + + const FilePairs result = ProjectExplorerPlugin::renameFiles(nodesToRename); + + QCOMPARE(result.size(), 1); + QCOMPARE(testProject.testBuildSystem()->canRenameFileCount, 1); + QCOMPARE(testProject.testBuildSystem()->renameFilesCount, 1); + QCOMPARE(testProject.sourceFile.exists(), false); + QCOMPARE(testRenamed.exists(), true); +} + +void ProjectExplorerTest::testProject_renameFile_NullNode() +{ + TemporaryDirectory tempDir("testProject_renameFile_NullNode"); + RenameTestProject testProject(tempDir); + testProject.initialize(); + const FilePath testRenamed = makeRenamedFilePath(testProject.sourceFile); + QList> nodesToRename{{nullptr, testRenamed}}; + + const FilePairs result = ProjectExplorerPlugin::renameFiles(nodesToRename); + + QVERIFY(result.isEmpty()); +} + +void ProjectExplorerTest::testProject_renameMultipleFiles() +{ + TemporaryDirectory tempDir("testProject_renameMultipleFiles"); + RenameTestProject testProject(tempDir); + testProject.initialize(); + const FilePath renamedFilePath1 = makeRenamedFilePath(testProject.sourceFile); + const FilePath renamedFilePath2 = makeRenamedFilePath(testProject.secondSourceFile); + auto testNode1 = const_cast(testProject.nodeForFilePath(testProject.sourceFile)); + auto testNode2 = const_cast(testProject.nodeForFilePath(testProject.secondSourceFile)); + QList> + nodesToRename{{testNode1, renamedFilePath1}, {testNode2, renamedFilePath2}}; + + const FilePairs result = ProjectExplorerPlugin::renameFiles(nodesToRename); + + QCOMPARE(result.size(), 2); + QCOMPARE(testProject.testBuildSystem()->canRenameFileCount, 2); + QCOMPARE(testProject.testBuildSystem()->renameFilesCount, 1); + QCOMPARE(testProject.sourceFile.exists(), false); + QCOMPARE(testProject.secondSourceFile.exists(), false); + QCOMPARE(renamedFilePath1.exists(), true); + QCOMPARE(renamedFilePath2.exists(), true); +} + +void ProjectExplorerTest::testProject_renameFile_BuildSystemRejectsAll() +{ + TemporaryDirectory tempDir("testProject_renameFile_BuildSystemRejectsAll"); + RenameTestProject testProject(tempDir); + testProject.initialize(); + Node *sourcNode = const_cast(testProject.nodeForFilePath(testProject.sourceFile)); + const FilePath renamedPath = makeRenamedFilePath(testProject.sourceFile); + QList> toRename{{sourcNode, renamedPath}}; + + const FilePairs result = ProjectExplorerPlugin::renameFiles(toRename); + + QVERIFY(result.isEmpty()); + QCOMPARE(testProject.testBuildSystem()->canRenameFileCount, 1); + QCOMPARE(testProject.testBuildSystem()->renameFilesCount, 1); + QCOMPARE(testProject.sourceFile.exists(), false); + QCOMPARE(renamedPath.exists(), true); +} + +void ProjectExplorerTest::testProject_renameFile_BuildSystemRejectsPartial() +{ + TemporaryDirectory tempDir("testProject_renameFile_BuildSystemRejectsPartial"); + RenameTestProject testProject(tempDir); + testProject.initialize(); + PartiallyRejectingRenameBuildSystem::pathsToReject = {testProject.sourceFile}; + const FilePath renamed1 = makeRenamedFilePath(testProject.sourceFile); + const FilePath renamed2 = makeRenamedFilePath(testProject.secondSourceFile); + Node *node1 = const_cast(testProject.nodeForFilePath(testProject.sourceFile)); + Node *node2 = const_cast(testProject.nodeForFilePath(testProject.secondSourceFile)); + QList> toRename{{node1, renamed1}, {node2, renamed2}}; + + const FilePairs result = ProjectExplorerPlugin::renameFiles(toRename); + + QCOMPARE(result.size(), 1); + QVERIFY(result.contains({testProject.secondSourceFile, renamed2})); + QCOMPARE(testProject.testBuildSystem()->canRenameFileCount, 2); + QCOMPARE(testProject.testBuildSystem()->renameFilesCount, 1); + QCOMPARE(testProject.sourceFile.exists(), false); + QCOMPARE(testProject.secondSourceFile.exists(), false); + QCOMPARE(renamed1.exists(), true); + QCOMPARE(renamed2.exists(), true); +} + +void ProjectExplorerTest::testProject_renameFile_QmlCrashSimulation() +{ + TemporaryDirectory tempDir("testProject_renameFile_QmlCrashSimulation"); + RenameTestProject testProject(tempDir); + testProject.initialize(); + ReparsingBuildSystem::projectToReparse = &testProject; + Node *sourceNode = const_cast(testProject.nodeForFilePath(testProject.sourceFile)); + const FilePath renamedPath = makeRenamedFilePath(testProject.sourceFile); + QList> toRename{{sourceNode, renamedPath}}; + + const FilePairs result = ProjectExplorerPlugin::renameFiles(toRename); + + QVERIFY(renamedPath.exists()); + QCOMPARE(testProject.sourceFile.exists(), false); + QVERIFY(result.isEmpty()); +} + void ProjectExplorerTest::testProject_multipleBuildConfigs() { // Find suitable kit. diff --git a/src/plugins/projectexplorer/projectexplorer.cpp b/src/plugins/projectexplorer/projectexplorer.cpp index aebc97270f4..922b5a25fdd 100644 --- a/src/plugins/projectexplorer/projectexplorer.cpp +++ b/src/plugins/projectexplorer/projectexplorer.cpp @@ -110,6 +110,7 @@ #include #include #include +#include #include #include #include @@ -2482,97 +2483,166 @@ void ProjectExplorerPlugin::showOutputPaneForRunControl(RunControl *runControl) appOutputPane().showOutputPaneForRunControl(runControl); } +static QList> collectValidRenames( + const QList> &nodesAndDesiredPaths, + QHash &oldPathToNode) +{ + QList> pendingRenames; + + for (const std::pair &nodeAndPath : nodesAndDesiredPaths) { + Node *originalNode = nodeAndPath.first; + if (!originalNode) + continue; + + const FilePath oldPath = originalNode->filePath(); + const FilePath newPath = nodeAndPath.second; + + if (oldPath.equalsCaseSensitive(newPath)) + continue; + + pendingRenames.append({oldPath, newPath}); + oldPathToNode.insert(oldPath, originalNode); + } + return pendingRenames; +} + static HandleIncludeGuards canTryToRenameIncludeGuards(const Node *node) { return node->asFileNode() && node->asFileNode()->fileType() == FileType::Header - ? HandleIncludeGuards::Yes : HandleIncludeGuards::No; + ? HandleIncludeGuards::Yes + : HandleIncludeGuards::No; +} + +static void applyFileSystemRenames( + const QList> &pendingRenames, + const QHash &oldPathToNode, + FilePairs &successfulRenames, + FilePaths &failedRenames) +{ + for (const std::pair &oldAndNew : pendingRenames) { + Node *originalNode = oldPathToNode.value(oldAndNew.first); + + const bool renameSucceeded = Core::FileUtils::renameFile( + oldAndNew.first, oldAndNew.second, canTryToRenameIncludeGuards(originalNode)); + + if (renameSucceeded) + successfulRenames.append(oldAndNew); + else + failedRenames.append(oldAndNew.first); + } +} + +static void applyProjectTreeRenames( + const FilePairs &fileSystemSuccessfulRenames, + const QHash &oldPathToNode, + FilePairs &completelySuccessfulRenames, + FilePaths &projectUpdateFailures, + FilePaths &skippedDueToInvalidContext) +{ + QHash renamesGroupedByParent; + + for (const std::pair &oldAndNew : fileSystemSuccessfulRenames) { + const FilePath &oldPath = oldAndNew.first; + const FilePath &newPath = oldAndNew.second; + + Node *originalNode = oldPathToNode.value(oldPath); + FolderNode *parentNode = originalNode ? originalNode->parentFolderNode() : nullptr; + + if (parentNode) { + parentNode->canRenameFile(oldPath, newPath); + renamesGroupedByParent[parentNode].append(oldAndNew); + } else { + skippedDueToInvalidContext.append(oldPath); + } + } + + for (auto it = renamesGroupedByParent.cbegin(); it != renamesGroupedByParent.cend(); ++it) { + FolderNode *folderNode = it.key(); + const FilePairs &filePairsForFolder = it.value(); + + FilePaths notRenamedInBuildSystem; + const bool buildSystemReportedSuccess + = folderNode->renameFiles(filePairsForFolder, ¬RenamedInBuildSystem); + + for (const std::pair &oldAndNew : filePairsForFolder) { + const FilePath &oldPath = oldAndNew.first; + + const bool updatedInProject = buildSystemReportedSuccess + && !notRenamedInBuildSystem.contains(oldPath); + if (updatedInProject) + completelySuccessfulRenames.append(oldAndNew); + else + projectUpdateFailures.append(oldPath); + } + } +} + +static void showRenameDiagnostics( + const FilePaths &fileSystemFailures, + const FilePaths &projectUpdateFailures, + const FilePaths &skippedRenames) +{ + if (fileSystemFailures.isEmpty() && projectUpdateFailures.isEmpty() + && skippedRenames.isEmpty()) { + return; + } + + const auto pathsToHtmlList = [](const FilePaths &paths) { + QString html = "
    "; + for (const FilePath &path : paths) + html += "
  • " + path.toUserOutput() + "
  • "; + return html += "
"; + }; + + QString messageBody; + if (!fileSystemFailures.isEmpty()) + messageBody += Tr::tr("The following files could not be renamed in the file system:%1") + .arg(pathsToHtmlList(fileSystemFailures)); + + if (!projectUpdateFailures.isEmpty()) { + if (!messageBody.isEmpty()) + messageBody += "
"; + messageBody += Tr::tr("These files were renamed in the file system, but project files were " + "not updated:%1") + .arg(pathsToHtmlList(projectUpdateFailures)); + } + + if (!skippedRenames.isEmpty()) { + if (!messageBody.isEmpty()) + messageBody += "
"; + messageBody += Tr::tr("These files were renamed in the file system, but the project " + "structure was not updated (context lost or unsupported):%1") + .arg(pathsToHtmlList(skippedRenames)); + } + + Core::AsynchronousMessageBox::warning(Tr::tr("Renaming Issues"), messageBody); } FilePairs ProjectExplorerPlugin::renameFiles( - const QList> &nodesAndNewFilePaths) + const QList> &nodesAndDesiredNewPaths) { - const QList> nodesAndNewFilePathsFiltered - = Utils::filtered(nodesAndNewFilePaths, [](const std::pair &elem) { - return !elem.first->filePath().equalsCaseSensitive(elem.second); - }); + QHash oldPathToNode; + const QList> pendingRenames + = collectValidRenames(nodesAndDesiredNewPaths, oldPathToNode); - // The same as above, for use when the nodes might no longer exist. - const QList> oldAndNewFilePathsFiltered - = Utils::transform(nodesAndNewFilePathsFiltered, [](const std::pair &p) { - return std::make_pair(p.first->filePath(), p.second); - }); + if (pendingRenames.isEmpty()) + return {}; - FilePaths renamedOnly; - FilePaths failedRenamings; - const auto renameFile = [&failedRenamings](const Node *node, const FilePath &newFilePath) { - if (!Core::FileUtils::renameFile( - node->filePath(), newFilePath, canTryToRenameIncludeGuards(node))) { - failedRenamings << node->filePath(); - return false; - } - return true; - }; - QHash>> renamingsPerParentNode; - for (const auto &elem : nodesAndNewFilePathsFiltered) { - if (FolderNode * const folderNode = elem.first->parentFolderNode()) - renamingsPerParentNode[folderNode] << elem; - else if (renameFile(elem.first, elem.second)) - renamedOnly << elem.first->filePath(); - } + FilePairs fileSystemSuccess; + FilePaths fileSystemFailures; + applyFileSystemRenames(pendingRenames, oldPathToNode, fileSystemSuccess, fileSystemFailures); - for (auto it = renamingsPerParentNode.cbegin(); it != renamingsPerParentNode.cend(); ++it) { - FilePairs toUpdateInProject; - for (const std::pair &elem : it.value()) { - const bool canUpdateProject - = it.key()->canRenameFile(elem.first->filePath(), elem.second); - if (renameFile(elem.first, elem.second)) { - if (canUpdateProject ) - toUpdateInProject << std::make_pair(elem.first->filePath(), elem.second); - else - renamedOnly << elem.first->filePath(); - } - } - if (toUpdateInProject.isEmpty()) - continue; - FilePaths notRenamed; - if (!it.key()->renameFiles(toUpdateInProject, ¬Renamed)) - renamedOnly << notRenamed; - } + FilePairs projectSuccess; + FilePaths projectFailures; + FilePaths skippedRenames; + applyProjectTreeRenames( + fileSystemSuccess, oldPathToNode, projectSuccess, projectFailures, skippedRenames); - if (!failedRenamings.isEmpty() || !renamedOnly.isEmpty()) { - const auto pathsAsHtmlList = [](const FilePaths &files) { - QString s("
    "); - for (const FilePath &f : files) - s.append("
  • ").append(f.toUserOutput()).append("
  • "); - return s.append("
"); - }; - QString failedRenamingsString; - if (!failedRenamings.isEmpty()) { - failedRenamingsString = Tr::tr("The following files could not be renamed: %1") - .arg(pathsAsHtmlList(failedRenamings)); - } - QString renamedOnlyString; - if (!renamedOnly.isEmpty()) { - renamedOnlyString - = "
" - + Tr::tr("The following files were renamed, but their project files could not " - "be updated accordingly: %1") - .arg(pathsAsHtmlList(renamedOnly)); - } - QTimer::singleShot( - 0, m_instance, [message = QString(failedRenamingsString + renamedOnlyString)] { - QMessageBox::warning( - ICore::dialogParent(), Tr::tr("Renaming Did Not Fully Succeed"), message); - }); - } + showRenameDiagnostics(fileSystemFailures, projectFailures, skippedRenames); - FilePairs allRenamedFiles; - for (const std::pair &candidate : oldAndNewFilePathsFiltered) { - if (!failedRenamings.contains(candidate.first)) - allRenamedFiles.emplaceBack(candidate.first, candidate.second); - } - emit instance()->filesRenamed(allRenamedFiles); - return allRenamedFiles; + if (!projectSuccess.isEmpty()) + emit instance()->filesRenamed(projectSuccess); + return projectSuccess; } #ifdef WITH_TESTS diff --git a/src/plugins/projectexplorer/projectexplorer_test.h b/src/plugins/projectexplorer/projectexplorer_test.h index bff41d08e9b..7e7995f5c3f 100644 --- a/src/plugins/projectexplorer/projectexplorer_test.h +++ b/src/plugins/projectexplorer/projectexplorer_test.h @@ -80,6 +80,13 @@ private slots: void testProject_parsingSuccess(); void testProject_parsingFail(); void testProject_projectTree(); + void testProject_renameFile(); + void testProject_renameFile_NullNode(); + void testProject_renameMultipleFiles(); + void testProject_renameFile_BuildSystemRejectsAll(); + void testProject_renameFile_BuildSystemRejectsPartial(); + void testProject_renameFile_QmlCrashSimulation(); + void testProject_multipleBuildConfigs(); void testSourceToBinaryMapping(); @@ -87,7 +94,6 @@ private slots: void testSessionSwitch(); -private: friend class ::ProjectExplorer::ProjectExplorerPlugin; }; diff --git a/src/plugins/projectexplorer/projectmodels.cpp b/src/plugins/projectexplorer/projectmodels.cpp index ace041c9e75..46ce86388de 100644 --- a/src/plugins/projectexplorer/projectmodels.cpp +++ b/src/plugins/projectexplorer/projectmodels.cpp @@ -412,6 +412,8 @@ void FlatModel::parsingStateChanged(Project *project) void FlatModel::updateSubtree(FolderNode *node) { + if (!node) + return; // FIXME: This is still excessive, should be limited to the affected subtree. while (FolderNode *parent = node->parentFolderNode()) node = parent;