From 54af6bd5b3f5ba5e3396f5cb9eb539f198abafff Mon Sep 17 00:00:00 2001 From: Cristian Adam Date: Fri, 21 Apr 2023 22:08:45 +0200 Subject: [PATCH] CMakePM: Allow files to be renamed in project view This includes both with source files explicitly specified or resulted from a file(GLOB|GLOB_RECOURSE) call. Fixes: QTCREATORBUG-27538 Change-Id: I5ee113af168bdb8cd0a96e8ab2ae603c0607fb0b Reviewed-by: hjk Reviewed-by: --- .../cmakeprojectmanager/cmakebuildsystem.cpp | 147 +++++++++++++++++- .../cmakeprojectmanager/cmakebuildsystem.h | 16 ++ .../hello-widgets/CMakeLists.txt | 5 + 3 files changed, 167 insertions(+), 1 deletion(-) diff --git a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp index 9cb3692d765..052ef4deeee 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp +++ b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp @@ -201,7 +201,8 @@ void CMakeBuildSystem::triggerParsing() bool CMakeBuildSystem::supportsAction(Node *context, ProjectAction action, const Node *node) const { if (dynamic_cast(context)) - return action == ProjectAction::AddNewFile || action == ProjectAction::AddExistingFile; + return action == ProjectAction::AddNewFile || action == ProjectAction::AddExistingFile + || action == ProjectAction::Rename; return BuildSystem::supportsAction(context, action, node); } @@ -366,6 +367,150 @@ bool CMakeBuildSystem::addFiles(Node *context, const FilePaths &filePaths, FileP return BuildSystem::addFiles(context, filePaths, notAdded); } +bool CMakeBuildSystem::canRenameFile(Node *context, + const FilePath &oldFilePath, + const FilePath &newFilePath) +{ + // "canRenameFile" will cause an actual rename after the function call. + // This will make the a sequence like + // canonicalPath().relativePathFrom(projDir).cleanPath().toString() + // to fail if the file doesn't exist on disk + // therefore cache the results for the subsequent "renameFile" call + // where oldFilePath has already been renamed as newFilePath. + + if (auto n = dynamic_cast(context)) { + const FilePath projDir = n->filePath().canonicalPath(); + const QString oldRelPathName + = oldFilePath.canonicalPath().relativePathFrom(projDir).cleanPath().toString(); + + const QString targetName = n->buildKey(); + auto target = Utils::findOrDefault(buildTargets(), + [targetName](const CMakeBuildTarget &target) { + return target.title == targetName; + }); + + if (target.backtrace.isEmpty()) { + return false; + } + const FilePath targetCMakeFile = target.backtrace.last().path; + + // Have a fresh look at the CMake file, not relying on a cached value + expected_str fileContent = targetCMakeFile.fileContents(); + cmListFile cmakeListFile; + std::string errorString; + if (fileContent) { + fileContent = fileContent->replace("\r\n", "\n"); + if (!cmakeListFile.ParseString(fileContent->toStdString(), + targetCMakeFile.fileName().toStdString(), + errorString)) + return false; + } + + const int targetDefinitionLine = target.backtrace.last().line; + + auto function = std::find_if(cmakeListFile.Functions.begin(), + cmakeListFile.Functions.end(), + [targetDefinitionLine](const auto &func) { + return func.Line() == targetDefinitionLine; + }); + + const std::string target_name = targetName.toStdString(); + auto targetSourcesFunc + = std::find_if(cmakeListFile.Functions.begin(), + cmakeListFile.Functions.end(), + [target_name = targetName.toStdString()](const auto &func) { + return func.LowerCaseName() == "target_sources" + && func.Arguments().front().Value == target_name; + }); + + for (const auto &func : {function, targetSourcesFunc}) { + if (func == cmakeListFile.Functions.end()) + continue; + auto filePathArgument + = Utils::findOrDefault(func->Arguments(), + [fileName = oldRelPathName.toStdString()](const auto &arg) { + return arg.Delim != cmListFileArgument::Comment + && arg.Value == fileName; + }); + + const QString key + = QStringList{projDir.path(), targetName, oldFilePath.path(), newFilePath.path()} + .join(";"); + + if (!filePathArgument.Value.empty()) { + m_filesToBeRenamed.insert(key, {filePathArgument, targetCMakeFile, oldRelPathName}); + return true; + } else { + const auto globFunctions = std::get<0>( + Utils::partition(cmakeListFile.Functions, [](const auto &f) { + return f.LowerCaseName() == "file" && f.Arguments().size() > 2 + && (f.Arguments().front().Value == "GLOB" + || f.Arguments().front().Value == "GLOB_RECURSE"); + })); + + const auto globVariables + = Utils::transform(globFunctions, [](const auto &func) { + return std::string("${") + func.Arguments()[1].Value + "}"; + }); + + const auto haveGlobbing + = Utils::anyOf(func->Arguments(), [globVariables](const auto &arg) { + return globVariables.contains(arg.Value) + && arg.Delim != cmListFileArgument::Comment; + }); + + if (haveGlobbing) { + m_filesToBeRenamed + .insert(key, {filePathArgument, targetCMakeFile, oldRelPathName, true}); + return true; + } + } + } + } + return false; +} + +bool CMakeBuildSystem::renameFile(Node *context, + const FilePath &oldFilePath, + const FilePath &newFilePath) +{ + if (auto n = dynamic_cast(context)) { + const FilePath projDir = n->filePath().canonicalPath(); + const QString newRelPathName + = newFilePath.canonicalPath().relativePathFrom(projDir).cleanPath().toString(); + + const QString targetName = n->buildKey(); + const QString key + = QStringList{projDir.path(), targetName, oldFilePath.path(), newFilePath.path()}.join( + ";"); + + auto fileToRename = m_filesToBeRenamed.take(key); + if (!fileToRename.cmakeFile.exists()) + return false; + + BaseTextEditor *editor = qobject_cast( + Core::EditorManager::openEditorAt({fileToRename.cmakeFile, + static_cast(fileToRename.argumentPosition.Line), + static_cast(fileToRename.argumentPosition.Column + - 1)}, + Constants::CMAKE_EDITOR_ID, + Core::EditorManager::DoNotMakeVisible)); + if (!editor) + return false; + + if (!fileToRename.fromGlobbing) + editor->replace(fileToRename.oldRelativeFileName.length(), newRelPathName); + + editor->editorWidget()->autoIndent(); + if (!Core::DocumentManager::saveDocument(editor->document())) + return false; + + return true; + } + + return false; +} + FilePaths CMakeBuildSystem::filesGeneratedFrom(const FilePath &sourceFile) const { FilePath project = projectDirectory(); diff --git a/src/plugins/cmakeprojectmanager/cmakebuildsystem.h b/src/plugins/cmakeprojectmanager/cmakebuildsystem.h index eea894f5e92..9566df6ada5 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildsystem.h +++ b/src/plugins/cmakeprojectmanager/cmakebuildsystem.h @@ -48,6 +48,13 @@ public: bool addFiles(ProjectExplorer::Node *context, const Utils::FilePaths &filePaths, Utils::FilePaths *) final; + bool canRenameFile(ProjectExplorer::Node *context, + const Utils::FilePath &oldFilePath, + const Utils::FilePath &newFilePath) final; + bool renameFile(ProjectExplorer::Node *context, + const Utils::FilePath &oldFilePath, + const Utils::FilePath &newFilePath) final; + Utils::FilePaths filesGeneratedFrom(const Utils::FilePath &sourceFile) const final; QString name() const final { return QLatin1String("cmake"); } @@ -201,6 +208,15 @@ private: QList m_buildTargets; QSet m_cmakeFiles; + struct FileToBeRenamed + { + cmListFileArgument argumentPosition; + Utils::FilePath cmakeFile; + QString oldRelativeFileName; + bool fromGlobbing = false; + }; + QHash m_filesToBeRenamed; + // Parsing state: BuildDirParameters m_parameters; int m_reparseParameters = REPARSE_DEFAULT; diff --git a/tests/manual/cmakeprojectmanager/hello-widgets/CMakeLists.txt b/tests/manual/cmakeprojectmanager/hello-widgets/CMakeLists.txt index 4400c291ef0..8c0c413d663 100644 --- a/tests/manual/cmakeprojectmanager/hello-widgets/CMakeLists.txt +++ b/tests/manual/cmakeprojectmanager/hello-widgets/CMakeLists.txt @@ -30,3 +30,8 @@ my_add_executable(hello-my-widgets ) target_link_libraries(hello-my-widgets PRIVATE Qt6::Widgets) + +file(GLOB SOURCE_FILES CONFIGURE_DEPENDS *.cpp *.h *.ui) + +add_executable(hello-widgets-glob ${SOURCE_FILES}) +target_link_libraries(hello-widgets-glob PRIVATE Qt6::Widgets)