CppEditor: Make renameIncludes() also work for moved files

Task-number: QTCREATORBUG-26545
Change-Id: I0bfe203af8f091562cdd91411dbe502fc5a76956
Reviewed-by: David Schulz <david.schulz@qt.io>
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
This commit is contained in:
Christian Kandeler
2024-03-14 10:24:46 +01:00
parent dd876dc405
commit 42edb0dd61
11 changed files with 108 additions and 41 deletions

View File

@@ -1878,16 +1878,15 @@ void CppModelManager::renameIncludes(const QList<std::pair<FilePath, FilePath>>
if (oldFilePath.isEmpty() || newFilePath.isEmpty())
continue;
// We just want to handle renamings so return when the file was actually moved.
if (oldFilePath.absolutePath() != newFilePath.absolutePath())
continue;
const TextEditor::PlainRefactoringFileFactory changes;
QString oldFileName = oldFilePath.fileName();
QString newFileName = newFilePath.fileName();
const bool isUiFile = oldFilePath.suffix() == "ui" && newFilePath.suffix() == "ui";
const bool moved = oldFilePath.absolutePath() != newFilePath.absolutePath();
if (isUiFile) {
if (moved)
return; // This is out of scope.
oldFileName = "ui_" + oldFilePath.baseName() + ".h";
newFileName = "ui_" + newFilePath.baseName() + ".h";
}
@@ -1925,12 +1924,26 @@ void CppModelManager::renameIncludes(const QList<std::pair<FilePath, FilePath>>
TextEditor::RefactoringFilePtr file = changes.file(includingFileNew);
const QTextBlock &block = file->document()->findBlockByNumber(loc.second - 1);
const int replaceStart = block.text().indexOf(oldFileName);
if (replaceStart > -1) {
const FilePath relPathOld = FilePath::fromString(FilePath::calcRelativePath(
oldFilePath.toString(), includingFileOld.parentDir().toString()));
const FilePath relPathNew = FilePath::fromString(FilePath::calcRelativePath(
newFilePath.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;
} else {
oldString = relPathOld.toString();
newString = relPathNew.toString();
}
if (replaceStart > -1 && oldString != newString) {
ChangeSet changeSet;
changeSet.replace(block.position() + replaceStart,
block.position() + replaceStart + oldFileName.length(),
newFileName);
block.position() + replaceStart + oldString.length(),
newString);
file->setChangeSet(changeSet);
file->apply();
}

View File

@@ -17,6 +17,7 @@
#include <cplusplus/LookupContext.h>
#include <projectexplorer/kitmanager.h>
#include <projectexplorer/projectexplorer.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/projectnodes.h>
@@ -965,50 +966,80 @@ void ModelManagerTest::testUpdateEditorsAfterProjectUpdate()
QCOMPARE(documentBProjectPart->topLevelProject, pi->projectFilePath());
}
void ModelManagerTest::testRenameIncludes_data()
{
QTest::addColumn<QString>("oldRelPath");
QTest::addColumn<QString>("newRelPath");
QTest::addColumn<bool>("successExpected");
QTest::addRow("rename in place 1")
<< "subdir1/header1.h" << "subdir1/header1_renamed.h" << true;
QTest::addRow("rename in place 2")
<< "subdir2/header2.h" << "subdir2/header2_renamed.h" << true;
QTest::addRow("rename in place 3") << "header.h" << "header_renamed.h" << true;
QTest::addRow("move up") << "subdir1/header1.h" << "header1_moved.h" << true;
QTest::addRow("move up (breaks build)") << "subdir2/header2.h" << "header2_moved.h" << false;
QTest::addRow("move down") << "header.h" << "subdir1/header_moved.h" << true;
QTest::addRow("move across") << "subdir1/header1.h" << "subdir2/header1_moved.h" << true;
QTest::addRow("move across (breaks build)")
<< "subdir2/header2.h" << "subdir1/header2_moved.h" << false;
}
void ModelManagerTest::testRenameIncludes()
{
struct ModelManagerGCHelper {
~ModelManagerGCHelper() { CppModelManager::GC(); }
} GCHelper;
Q_UNUSED(GCHelper) // do not warn about being unused
// 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");
const FilePath projectFile = projectDir.pathAppended(projectDir.fileName() + ".pro");
ProjectOpenerAndCloser projectMgr;
QVERIFY(projectMgr.open(projectFile, true, kit));
const QDir workingDir(tmpDir.path());
const QStringList fileNames = {"foo.h", "foo.cpp", "main.cpp"};
const FilePath oldHeader = FilePath::fromString(workingDir.filePath("foo.h"));
const FilePath newHeader = FilePath::fromString(workingDir.filePath("bar.h"));
const MyTestDataDir testDir(_("testdata_project1"));
// Copy test files to a temporary directory
QSet<FilePath> sourceFiles;
for (const QString &fileName : std::as_const(fileNames)) {
const QString &file = workingDir.filePath(fileName);
QVERIFY(QFile::copy(testDir.file(fileName), file));
// Saving source file names for the model manager update,
// so we can update just the relevant files.
if (ProjectFile::classify(file) == ProjectFile::CXXSource)
sourceFiles.insert(FilePath::fromString(file));
}
// Update the c++ model manager and check for the old includes
CppModelManager::updateSourceFiles(sourceFiles).waitForFinished();
QCoreApplication::processEvents();
// 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 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"});
CPlusPlus::Snapshot snapshot = CppModelManager::snapshot();
for (const FilePath &sourceFile : std::as_const(sourceFiles)) {
QCOMPARE(snapshot.allIncludesForDocument(sourceFile), QSet<FilePath>{oldHeader});
for (const FilePath &srcFile : allSources) {
QCOMPARE(snapshot.allIncludesForDocument(srcFile), allHeaders);
}
// Renaming the header
// Rename the header.
QFETCH(QString, oldRelPath);
QFETCH(QString, newRelPath);
QFETCH(bool, successExpected);
const FilePath oldHeader = projectDir.pathAppended(oldRelPath);
const FilePath newHeader = projectDir.pathAppended(newRelPath);
QVERIFY(ProjectExplorerPlugin::renameFile(oldHeader, newHeader));
// Update the c++ model manager again and check for the new includes
CppModelManager::updateSourceFiles(sourceFiles).waitForFinished();
QCoreApplication::processEvents();
// Verify new code model state.
QVERIFY(::CppEditor::Tests::waitForSignalOrTimeout(
CppModelManager::instance(), &CppModelManager::sourceFilesRefreshed, 10000));
QSet<FilePath> incompleteNewHeadersSet = allHeaders;
incompleteNewHeadersSet.remove(oldHeader);
QSet<FilePath> completeNewHeadersSet = incompleteNewHeadersSet;
completeNewHeadersSet << newHeader;
snapshot = CppModelManager::snapshot();
for (const FilePath &sourceFile : std::as_const(sourceFiles)) {
QCOMPARE(snapshot.allIncludesForDocument(sourceFile), QSet<FilePath>{newHeader});
for (const FilePath &srcFile : allSources) {
const QSet<FilePath> &expectedHeaders = srcFile.fileName() == "main.cpp" && !successExpected
? incompleteNewHeadersSet : completeNewHeadersSet;
QCOMPARE(snapshot.allIncludesForDocument(srcFile), expectedHeaders);
}
}

View File

@@ -28,6 +28,7 @@ private slots:
void testDefinesPerEditor();
void testUpdateEditorsAfterProjectUpdate();
void testPrecompiledHeaders();
void testRenameIncludes_data();
void testRenameIncludes();
void testRenameIncludesInEditor();
void testDocumentsAndRevisions();

View File

@@ -802,6 +802,10 @@ bool FlatModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int r
if (vcsAddPossible && !targetVcs.vcs->vcsAdd(targetFile))
failedVcsOp << targetFile;
}
QList<std::pair<FilePath, FilePath>> renameList;
for (int i = 0; i < filesToAdd.size(); ++i)
renameList.emplaceBack(filesToRemove.at(i), filesToAdd.at(i));
emit ProjectExplorerPlugin::instance()->filesRenamed(renameList);
const RemovedFilesFromProject result
= sourceProjectNode->removeFiles(filesToRemove, &failedRemoveFromProject);
if (result == RemovedFilesFromProject::Wildcard)

View File

@@ -0,0 +1,5 @@
#include "header.h"
#include "subdir1/header1.h"
#include <header2.h>
int main() {}

View File

@@ -0,0 +1,3 @@
#include "header1.h"
#include "../header.h"
#include "../subdir2/header2.h"

View File

@@ -0,0 +1,3 @@
#include "header2.h"
#include "../header.h"
#include "../subdir1/header1.h"

View File

@@ -0,0 +1,7 @@
TEMPLATE = app
TARGET = testdata_renameheaders
INCLUDEPATH += subdir2
CONFIG += no_include_pwd
HEADERS = header.h subdir1/header1.h subdir2/header2.h
SOURCES = main.cpp subdir1/file1.cpp subdir2/file2.cpp