diff options
author | Christian Kandeler <christian.kandeler@qt.io> | 2024-04-11 18:17:27 +0200 |
---|---|---|
committer | Christian Kandeler <christian.kandeler@qt.io> | 2024-04-26 08:14:21 +0000 |
commit | 77a46dc82613caf3e67498c4fc12cb6a5f64bd68 (patch) | |
tree | 4727c6a7706a677c09a60db6521e76a7763a54b2 | |
parent | 5c765e7c125ceba1d13d54ae0a452ff22d151c8e (diff) |
CppEditor: Adapt includes also when the including file was moved
Task-number: QTCREATORBUG-26545
Change-Id: Ica2d8c8504387f4ab15f0a974dfc1566d1fcaa91
Reviewed-by: David Schulz <david.schulz@qt.io>
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
-rw-r--r-- | src/plugins/cppeditor/cppmodelmanager.cpp | 81 | ||||
-rw-r--r-- | src/plugins/cppeditor/cppmodelmanager_test.cpp | 92 | ||||
-rw-r--r-- | src/plugins/cppeditor/cppmodelmanager_test.h | 2 | ||||
-rw-r--r-- | src/plugins/cppeditor/cpptoolstestcase.h | 2 | ||||
-rw-r--r-- | src/plugins/projectexplorer/projectexplorer.cpp | 20 | ||||
-rw-r--r-- | src/plugins/projectexplorer/projectexplorer.h | 3 |
6 files changed, 176 insertions, 24 deletions
diff --git a/src/plugins/cppeditor/cppmodelmanager.cpp b/src/plugins/cppeditor/cppmodelmanager.cpp index 4a2d4778b9..ac7fe32b91 100644 --- a/src/plugins/cppeditor/cppmodelmanager.cpp +++ b/src/plugins/cppeditor/cppmodelmanager.cpp @@ -77,6 +77,7 @@ #include <QDebug> #include <QDir> #include <QFutureWatcher> +#include <QHash> #include <QMutexLocker> #include <QReadLocker> #include <QReadWriteLock> @@ -1871,12 +1872,22 @@ QSet<QString> CppModelManager::internalTargets(const FilePath &filePath) // the renamings yet, i.e. they still refer to the old file paths. void CppModelManager::renameIncludes(const QList<std::pair<FilePath, FilePath>> &oldAndNewPaths) { + using IncludingFile = std::pair<FilePath, FilePath>; // old and new path; might be identical + struct RewriteCandidate { + FilePath oldHeaderFilePath; + FilePath newHeaderFilePath; + QString oldHeaderFileName; + QString newHeaderFileName; + int includeLine; + bool isUiHeader; + }; + QHash<IncludingFile, QList<RewriteCandidate>> rewriteCandidates; + + const Snapshot theSnapshot = snapshot(); for (const auto &[oldFilePath, newFilePath] : oldAndNewPaths) { if (oldFilePath.isEmpty() || newFilePath.isEmpty()) continue; - const TextEditor::PlainRefactoringFileFactory changes; - QString oldFileName = oldFilePath.fileName(); QString newFileName = newFilePath.fileName(); const bool isUiFile = oldFilePath.suffix() == "ui" && newFilePath.suffix() == "ui"; @@ -1902,7 +1913,8 @@ void CppModelManager::renameIncludes(const QList<std::pair<FilePath, FilePath>> if (isUiFile && !productNodeForUiFile) continue; - const QList<Snapshot::IncludeLocation> locations = snapshot().includeLocationsOfDocument( + // Handle locations where this file is included. + const QList<Snapshot::IncludeLocation> locations = theSnapshot.includeLocationsOfDocument( isUiFile ? FilePath::fromString(oldFileName) : oldFilePath); for (const Snapshot::IncludeLocation &loc : locations) { const FilePath includingFileOld = loc.first->filePath(); @@ -1919,32 +1931,73 @@ void CppModelManager::renameIncludes(const QList<std::pair<FilePath, FilePath>> if (isUiFile && getProductNode(includingFileOld) != productNodeForUiFile) continue; - TextEditor::RefactoringFilePtr file = changes.file(includingFileNew); - const QTextBlock &block = file->document()->findBlockByNumber(loc.second - 1); + rewriteCandidates[std::make_pair(includingFileOld, includingFileNew)].append( + {oldFilePath, newFilePath, oldFileName, newFileName, loc.second, isUiFile}); + } + + // Handle the includes of this file: If its location changed, its own local includes + // need to get adapted as well. + if (isUiFile || !moved) + continue; + const CPlusPlus::Document::Ptr cppDoc = theSnapshot.document(oldFilePath); + if (!cppDoc) + continue; + const auto key = std::make_pair(oldFilePath, newFilePath); + for (const CPlusPlus::Document::Include &include : cppDoc->resolvedIncludes()) { + const FilePath oldHeaderPath = include.resolvedFileName(); + const QString oldHeaderName = oldHeaderPath.fileName(); + const bool isUiHeader = oldHeaderName.startsWith("ui_") && oldHeaderName.endsWith(".h"); + if (!isUiHeader && include.type() != CPlusPlus::Client::IncludeLocal) + continue; + FilePath newHeaderPath = oldHeaderPath; + for (const auto &pathPair : oldAndNewPaths) { + if (oldHeaderPath == pathPair.first) { + newHeaderPath = pathPair.second; + break; + } + } + rewriteCandidates[key].append( + {oldHeaderPath, + newHeaderPath, + oldHeaderName, + newHeaderPath.fileName(), + include.line(), + isUiHeader}); + } + } + + const TextEditor::PlainRefactoringFileFactory changes; + for (auto it = rewriteCandidates.cbegin(); it != rewriteCandidates.cend(); ++it) { + const FilePath &includingFileOld = it.key().first; + const FilePath &includingFileNew = it.key().second; + TextEditor::RefactoringFilePtr file = changes.file(includingFileNew); + ChangeSet changeSet; + for (const RewriteCandidate &candidate : it.value()) { + const QTextBlock &block = file->document()->findBlockByNumber( + candidate.includeLine - 1); const FilePath relPathOld = FilePath::fromString(FilePath::calcRelativePath( - oldFilePath.toString(), includingFileOld.parentDir().toString())); + candidate.oldHeaderFilePath.toString(), includingFileOld.parentDir().toString())); const FilePath relPathNew = FilePath::fromString(FilePath::calcRelativePath( - newFilePath.toString(), includingFileNew.parentDir().toString())); + candidate.newHeaderFilePath.toString(), includingFileNew.parentDir().toString())); int replaceStart = block.text().indexOf(relPathOld.toString()); QString oldString; QString newString; - if (isUiFile || replaceStart == -1) { - replaceStart = block.text().indexOf(oldFileName); - oldString = oldFileName; - newString = newFileName; + if (candidate.isUiHeader || replaceStart == -1) { + replaceStart = block.text().indexOf(candidate.oldHeaderFileName); + oldString = candidate.oldHeaderFileName; + newString = candidate.newHeaderFileName; } else { oldString = relPathOld.toString(); newString = relPathNew.toString(); } if (replaceStart > -1 && oldString != newString) { - ChangeSet changeSet; changeSet.replace(block.position() + replaceStart, block.position() + replaceStart + oldString.length(), newString); - file->setChangeSet(changeSet); - file->apply(); } } + file->setChangeSet(changeSet); + file->apply(); } } diff --git a/src/plugins/cppeditor/cppmodelmanager_test.cpp b/src/plugins/cppeditor/cppmodelmanager_test.cpp index dadeacc05f..eda55f09ff 100644 --- a/src/plugins/cppeditor/cppmodelmanager_test.cpp +++ b/src/plugins/cppeditor/cppmodelmanager_test.cpp @@ -985,6 +985,30 @@ void ModelManagerTest::testRenameIncludes_data() << "subdir2/header2.h" << "subdir1/header2_moved.h" << false; } +class SourceFilesRefreshGuard : public QObject +{ +public: + SourceFilesRefreshGuard() + { + connect(CppModelManager::instance(), &CppModelManager::sourceFilesRefreshed, this, [this] { + m_refreshed = true; + }); + } + + void reset() { m_refreshed = false; } + bool wait() + { + for (int i = 0; i < 10 && !m_refreshed; ++i) { + CppEditor::Tests::waitForSignalOrTimeout( + CppModelManager::instance(), &CppModelManager::sourceFilesRefreshed, 1000); + } + return m_refreshed; + } + +private: + bool m_refreshed = false; +}; + void ModelManagerTest::testRenameIncludes() { // Set up project. @@ -1003,8 +1027,11 @@ void ModelManagerTest::testRenameIncludes() if (!kit) QSKIP("The test requires at least one valid kit with a valid Qt"); const FilePath projectFile = projectDir.pathAppended(projectDir.fileName() + ".pro"); + SourceFilesRefreshGuard refreshGuard; ProjectOpenerAndCloser projectMgr; - QVERIFY(projectMgr.open(projectFile, true, kit)); + const ProjectInfo::ConstPtr projectInfo = projectMgr.open(projectFile, true, kit); + QVERIFY(projectInfo); + QVERIFY(refreshGuard.wait()); // Verify initial code model state. const auto makeAbs = [&](const QStringList &relPaths) { @@ -1014,6 +1041,7 @@ void ModelManagerTest::testRenameIncludes() }; const QSet<FilePath> allSources = makeAbs({"main.cpp", "subdir1/file1.cpp", "subdir2/file2.cpp"}); const QSet<FilePath> allHeaders = makeAbs({"header.h", "subdir1/header1.h", "subdir2/header2.h"}); + QCOMPARE(projectInfo->sourceFiles(), allSources + allHeaders); CPlusPlus::Snapshot snapshot = CppModelManager::snapshot(); for (const FilePath &srcFile : allSources) { QCOMPARE(snapshot.allIncludesForDocument(srcFile), allHeaders); @@ -1025,11 +1053,11 @@ void ModelManagerTest::testRenameIncludes() QFETCH(bool, successExpected); const FilePath oldHeader = projectDir.pathAppended(oldRelPath); const FilePath newHeader = projectDir.pathAppended(newRelPath); + refreshGuard.reset(); QVERIFY(ProjectExplorerPlugin::renameFile(oldHeader, newHeader)); // Verify new code model state. - QVERIFY(::CppEditor::Tests::waitForSignalOrTimeout( - CppModelManager::instance(), &CppModelManager::sourceFilesRefreshed, 10000)); + QVERIFY(refreshGuard.wait()); QSet<FilePath> incompleteNewHeadersSet = allHeaders; incompleteNewHeadersSet.remove(oldHeader); QSet<FilePath> completeNewHeadersSet = incompleteNewHeadersSet; @@ -1043,6 +1071,64 @@ void ModelManagerTest::testRenameIncludes() } } +void ModelManagerTest::testMoveIncludingSources_data() +{ + QTest::addColumn<QString>("oldRelPath"); + QTest::addColumn<QString>("newRelPath"); + + QTest::addRow("move up") << "subdir1/file1.cpp" << "file1_moved.cpp"; + QTest::addRow("move down") << "main.cpp" << "subdir1/main.cpp"; + QTest::addRow("move across") << "subdir1/file1.cpp" << "subdir2/file1_moved.cpp"; +} + +void ModelManagerTest::testMoveIncludingSources() +{ + QFETCH(QString, oldRelPath); + QFETCH(QString, newRelPath); + + // Set up project. + TemporaryDir tmpDir; + QVERIFY(tmpDir.isValid()); + const MyTestDataDir sourceDir("testdata_renameheaders"); + const FilePath srcFilePath = FilePath::fromString(sourceDir.path()); + const FilePath projectDir = tmpDir.filePath().pathAppended(srcFilePath.fileName()); + const auto copyResult = srcFilePath.copyRecursively(projectDir); + if (!copyResult) + qDebug() << copyResult.error(); + QVERIFY(copyResult); + Kit * const kit = Utils::findOr(KitManager::kits(), nullptr, [](const Kit *k) { + return k->isValid() && !k->hasWarning() && k->value("QtSupport.QtInformation").isValid(); + }); + if (!kit) + QSKIP("The test requires at least one valid kit with a valid Qt"); + SourceFilesRefreshGuard refreshGuard; + const FilePath projectFile = projectDir.pathAppended(projectDir.fileName() + ".pro"); + ProjectOpenerAndCloser projectMgr; + QVERIFY(projectMgr.open(projectFile, true, kit)); + QVERIFY(refreshGuard.wait()); + + // Verify initial code model state. + const auto makeAbs = [&](const QStringList &relPaths) { + return Utils::transform<QSet<FilePath>>(relPaths, [&](const QString &relPath) { + return projectDir.pathAppended(relPath); + }); + }; + const FilePath oldSource = projectDir.pathAppended(oldRelPath); + QVERIFY(oldSource.exists()); + const QSet<FilePath> includedHeaders = makeAbs( + {"header.h", "subdir1/header1.h", "subdir2/header2.h"}); + QCOMPARE(CppModelManager::snapshot().allIncludesForDocument(oldSource), includedHeaders); + + // Rename the source file. + refreshGuard.reset(); + const FilePath newSource = projectDir.pathAppended(newRelPath); + QVERIFY(ProjectExplorerPlugin::renameFile(oldSource, newSource, projectMgr.projects().first())); + + // Verify new code model state. + QVERIFY(refreshGuard.wait()); + QCOMPARE(CppModelManager::snapshot().allIncludesForDocument(newSource), includedHeaders); +} + void ModelManagerTest::testRenameIncludesInEditor() { struct ModelManagerGCHelper { diff --git a/src/plugins/cppeditor/cppmodelmanager_test.h b/src/plugins/cppeditor/cppmodelmanager_test.h index 14e9c617de..6bcf25bc8e 100644 --- a/src/plugins/cppeditor/cppmodelmanager_test.h +++ b/src/plugins/cppeditor/cppmodelmanager_test.h @@ -30,6 +30,8 @@ private slots: void testPrecompiledHeaders(); void testRenameIncludes_data(); void testRenameIncludes(); + void testMoveIncludingSources_data(); + void testMoveIncludingSources(); void testRenameIncludesInEditor(); void testDocumentsAndRevisions(); void testSettingsChanges(); diff --git a/src/plugins/cppeditor/cpptoolstestcase.h b/src/plugins/cppeditor/cpptoolstestcase.h index 99969621b3..3bbeff0bb6 100644 --- a/src/plugins/cppeditor/cpptoolstestcase.h +++ b/src/plugins/cppeditor/cpptoolstestcase.h @@ -171,6 +171,8 @@ public: bool configureAsExampleProject = false, ProjectExplorer::Kit *kit = nullptr); + QList<ProjectExplorer::Project *> projects() const { return m_openProjects; }; + private: QList<ProjectExplorer::Project *> m_openProjects; }; diff --git a/src/plugins/projectexplorer/projectexplorer.cpp b/src/plugins/projectexplorer/projectexplorer.cpp index fced3700d0..385c38405a 100644 --- a/src/plugins/projectexplorer/projectexplorer.cpp +++ b/src/plugins/projectexplorer/projectexplorer.cpp @@ -2417,12 +2417,20 @@ QList<std::pair<FilePath, FilePath>> ProjectExplorerPlugin::renameFiles( } #ifdef WITH_TESTS -bool ProjectExplorerPlugin::renameFile(const Utils::FilePath &source, const Utils::FilePath &target) -{ - const bool success = Core::FileUtils::renameFile(source, target, HandleIncludeGuards::Yes); - if (success) - emit ProjectExplorerPlugin::instance()->filesRenamed({std::make_pair(source, target)}); - return success; +bool ProjectExplorerPlugin::renameFile(const Utils::FilePath &source, const Utils::FilePath &target, + Project *project) +{ + if (!project) { + const bool success = Core::FileUtils::renameFile(source, target, HandleIncludeGuards::Yes); + if (success) + emit ProjectExplorerPlugin::instance()->filesRenamed({std::make_pair(source, target)}); + return success; + } + Node * const sourceNode = const_cast<Node *>(project->nodeForFilePath(source)); + if (!sourceNode) + return false; + return !renameFiles({std::make_pair(sourceNode, target)}).isEmpty(); + } #endif // WITH_TESTS diff --git a/src/plugins/projectexplorer/projectexplorer.h b/src/plugins/projectexplorer/projectexplorer.h index 8f17af13df..0c737aa9ab 100644 --- a/src/plugins/projectexplorer/projectexplorer.h +++ b/src/plugins/projectexplorer/projectexplorer.h @@ -131,7 +131,8 @@ public: renameFiles(const QList<std::pair<Node *, Utils::FilePath>> &nodesAndNewFilePaths); #ifdef WITH_TESTS - static bool renameFile(const Utils::FilePath &source, const Utils::FilePath &target); + static bool renameFile(const Utils::FilePath &source, const Utils::FilePath &target, + Project *project = nullptr); #endif static QStringList projectFilePatterns(); |