aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristian Kandeler <christian.kandeler@qt.io>2024-04-11 18:17:27 +0200
committerChristian Kandeler <christian.kandeler@qt.io>2024-04-26 08:14:21 +0000
commit77a46dc82613caf3e67498c4fc12cb6a5f64bd68 (patch)
tree4727c6a7706a677c09a60db6521e76a7763a54b2
parent5c765e7c125ceba1d13d54ae0a452ff22d151c8e (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.cpp81
-rw-r--r--src/plugins/cppeditor/cppmodelmanager_test.cpp92
-rw-r--r--src/plugins/cppeditor/cppmodelmanager_test.h2
-rw-r--r--src/plugins/cppeditor/cpptoolstestcase.h2
-rw-r--r--src/plugins/projectexplorer/projectexplorer.cpp20
-rw-r--r--src/plugins/projectexplorer/projectexplorer.h3
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();