// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "projectmanager.h" #include "buildconfiguration.h" #include "editorconfiguration.h" #include "project.h" #include "projectexplorer.h" #include "projectexplorerconstants.h" #include "projectexplorertr.h" #include "projectmanager.h" #include "projectnodes.h" #include "target.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef WITH_TESTS #include "projectexplorer_test.h" #include #include #include #endif using namespace Core; using namespace Utils; using namespace ProjectExplorer::Internal; namespace ProjectExplorer { class ProjectManagerPrivate { public: void loadSession(); void saveSession(); void restoreDependencies(); void restoreStartupProject(); void restoreProjects(const FilePaths &fileList); void askUserAboutFailedProjects(); bool recursiveDependencyCheck(const FilePath &newDep, const FilePath &checkDep) const; FilePaths dependencies(const FilePath &proName) const; FilePaths dependenciesOrder() const; void dependencies(const FilePath &proName, FilePaths &result) const; static QString windowTitleAddition(const FilePath &filePath); static QString sessionTitle(const FilePath &filePath); bool hasProjects() const { return !m_projects.isEmpty(); } bool m_casadeSetActive = false; Project *m_startupProject = nullptr; QList m_projects; FilePaths m_failedProjects; QMap m_depMap; private: static QString locationInProject(const FilePath &filePath); }; static ProjectManager *m_instance = nullptr; static ProjectManagerPrivate *d = nullptr; static QString projectFolderId(Project *pro) { return pro->projectFilePath().toString(); } const int PROJECT_SORT_VALUE = 100; ProjectManager::ProjectManager() { m_instance = this; d = new ProjectManagerPrivate; connect(EditorManager::instance(), &EditorManager::editorCreated, this, &ProjectManager::configureEditor); connect(this, &ProjectManager::projectAdded, EditorManager::instance(), &EditorManager::updateWindowTitles); connect(this, &ProjectManager::projectRemoved, EditorManager::instance(), &EditorManager::updateWindowTitles); connect(this, &ProjectManager::projectDisplayNameChanged, EditorManager::instance(), &EditorManager::updateWindowTitles); EditorManager::setWindowTitleAdditionHandler(&ProjectManagerPrivate::windowTitleAddition); EditorManager::setSessionTitleHandler(&ProjectManagerPrivate::sessionTitle); connect(SessionManager::instance(), &SessionManager::aboutToLoadSession, this, [] { d->loadSession(); }); connect(SessionManager::instance(), &SessionManager::aboutToSaveSession, this, [] { d->saveSession(); }); } ProjectManager::~ProjectManager() { EditorManager::setWindowTitleAdditionHandler({}); EditorManager::setSessionTitleHandler({}); delete d; d = nullptr; } ProjectManager *ProjectManager::instance() { return m_instance; } bool ProjectManagerPrivate::recursiveDependencyCheck(const FilePath &newDep, const FilePath &checkDep) const { if (newDep == checkDep) return false; const FilePaths depList = m_depMap.value(checkDep); for (const FilePath &dependency : depList) { if (!recursiveDependencyCheck(newDep, dependency)) return false; } return true; } /* * The dependency management exposes an interface based on projects, but * is internally purely string based. This is suboptimal. Probably it would be * nicer to map the filenames to projects on load and only map it back to * filenames when saving. */ QList ProjectManager::dependencies(const Project *project) { const FilePath proName = project->projectFilePath(); const FilePaths proDeps = d->m_depMap.value(proName); QList projects; for (const FilePath &dep : proDeps) { Project *pro = Utils::findOrDefault(d->m_projects, [&dep](Project *p) { return p->projectFilePath() == dep; }); if (pro) projects += pro; } return projects; } bool ProjectManager::hasDependency(const Project *project, const Project *depProject) { const FilePath proName = project->projectFilePath(); const FilePath depName = depProject->projectFilePath(); const FilePaths proDeps = d->m_depMap.value(proName); return proDeps.contains(depName); } bool ProjectManager::canAddDependency(const Project *project, const Project *depProject) { const FilePath newDep = project->projectFilePath(); const FilePath checkDep = depProject->projectFilePath(); return d->recursiveDependencyCheck(newDep, checkDep); } bool ProjectManager::addDependency(Project *project, Project *depProject) { const FilePath proName = project->projectFilePath(); const FilePath depName = depProject->projectFilePath(); // check if this dependency is valid if (!d->recursiveDependencyCheck(proName, depName)) return false; FilePaths proDeps = d->m_depMap.value(proName); if (!proDeps.contains(depName)) { proDeps.append(depName); d->m_depMap[proName] = proDeps; } emit m_instance->dependencyChanged(project, depProject); return true; } void ProjectManager::removeDependency(Project *project, Project *depProject) { const FilePath proName = project->projectFilePath(); const FilePath depName = depProject->projectFilePath(); FilePaths proDeps = d->m_depMap.value(proName); proDeps.removeAll(depName); if (proDeps.isEmpty()) d->m_depMap.remove(proName); else d->m_depMap[proName] = proDeps; emit m_instance->dependencyChanged(project, depProject); } bool ProjectManager::isProjectConfigurationCascading() { return d->m_casadeSetActive; } void ProjectManager::setProjectConfigurationCascading(bool b) { d->m_casadeSetActive = b; SessionManager::markSessionFileDirty(); } void ProjectManager::setStartupProject(Project *startupProject) { QTC_ASSERT((!startupProject && d->m_projects.isEmpty()) || (startupProject && d->m_projects.contains(startupProject)), return); if (d->m_startupProject == startupProject) return; d->m_startupProject = startupProject; if (d->m_startupProject && d->m_startupProject->needsConfiguration()) { ModeManager::activateMode(Constants::MODE_SESSION); ModeManager::setFocusToCurrentMode(); } FolderNavigationWidgetFactory::setFallbackSyncFilePath( startupProject ? startupProject->projectFilePath().parentDir() : FilePath()); emit m_instance->startupProjectChanged(startupProject); } Project *ProjectManager::startupProject() { return d->m_startupProject; } Target *ProjectManager::startupTarget() { return d->m_startupProject ? d->m_startupProject->activeTarget() : nullptr; } BuildSystem *ProjectManager::startupBuildSystem() { Target *t = startupTarget(); return t ? t->buildSystem() : nullptr; } /*! * Returns the RunConfiguration of the currently active target * of the startup project, if such exists, or \c nullptr otherwise. */ RunConfiguration *ProjectManager::startupRunConfiguration() { Target *t = startupTarget(); return t ? t->activeRunConfiguration() : nullptr; } void ProjectManager::addProject(Project *pro) { QTC_ASSERT(pro, return); QTC_CHECK(!pro->displayName().isEmpty()); QTC_CHECK(pro->id().isValid()); SessionManager::markSessionFileDirty(); QTC_ASSERT(!d->m_projects.contains(pro), return); d->m_projects.append(pro); connect(pro, &Project::displayNameChanged, m_instance, [pro]() { emit m_instance->projectDisplayNameChanged(pro); }); emit m_instance->projectAdded(pro); const auto updateFolderNavigation = [pro] { // destructing projects might trigger changes, so check if the project is actually there if (QTC_GUARD(d->m_projects.contains(pro))) { const QIcon icon = pro->rootProjectNode() ? pro->rootProjectNode()->icon() : QIcon(); FolderNavigationWidgetFactory::insertRootDirectory({projectFolderId(pro), PROJECT_SORT_VALUE, pro->displayName(), pro->projectFilePath().parentDir(), icon}); } }; updateFolderNavigation(); configureEditors(pro); connect(pro, &Project::fileListChanged, m_instance, [pro, updateFolderNavigation]() { configureEditors(pro); updateFolderNavigation(); // update icon }); connect(pro, &Project::displayNameChanged, m_instance, updateFolderNavigation); if (!startupProject()) setStartupProject(pro); } void ProjectManager::removeProject(Project *project) { SessionManager::markSessionFileDirty(); QTC_ASSERT(project, return); removeProjects({project}); } void ProjectManagerPrivate::saveSession() { // save the startup project if (d->m_startupProject) SessionManager::setSessionValue("StartupProject", m_startupProject->projectFilePath().toSettings()); FilePaths projectFiles = Utils::transform(m_projects, &Project::projectFilePath); // Restore information on projects that failed to load: // don't read projects to the list, which the user loaded for (const FilePath &failed : std::as_const(m_failedProjects)) { if (!projectFiles.contains(failed)) projectFiles << failed; } SessionManager::setSessionValue("ProjectList", Utils::transform(projectFiles, &FilePath::toString)); SessionManager::setSessionValue("CascadeSetActive", m_casadeSetActive); QVariantMap depMap; auto i = m_depMap.constBegin(); while (i != m_depMap.constEnd()) { QString key = i.key().toString(); QStringList values; const FilePaths valueList = i.value(); for (const FilePath &value : valueList) values << value.toString(); depMap.insert(key, values); ++i; } SessionManager::setSessionValue("ProjectDependencies", QVariant(depMap)); } /*! Closes all projects */ void ProjectManager::closeAllProjects() { removeProjects(projects()); } const QList ProjectManager::projects() { return d->m_projects; } bool ProjectManager::hasProjects() { return d->hasProjects(); } bool ProjectManager::hasProject(Project *p) { return d->m_projects.contains(p); } FilePaths ProjectManagerPrivate::dependencies(const FilePath &proName) const { FilePaths result; dependencies(proName, result); return result; } void ProjectManagerPrivate::dependencies(const FilePath &proName, FilePaths &result) const { const FilePaths depends = m_depMap.value(proName); for (const FilePath &dep : depends) dependencies(dep, result); if (!result.contains(proName)) result.append(proName); } QString ProjectManagerPrivate::sessionTitle(const FilePath &filePath) { const QString sessionName = SessionManager::activeSession(); if (SessionManager::isDefaultSession(sessionName)) { if (filePath.isEmpty()) { // use single project's name if there is only one loaded. const QList projects = ProjectManager::projects(); if (projects.size() == 1) return projects.first()->displayName(); } } else { return sessionName.isEmpty() ? Tr::tr("Untitled") : sessionName; } return {}; } QString ProjectManagerPrivate::locationInProject(const FilePath &filePath) { const Project *project = ProjectManager::projectForFile(filePath); if (!project) return {}; const FilePath parentDir = filePath.parentDir(); if (parentDir == project->projectDirectory()) return "@ " + project->displayName(); if (filePath.isChildOf(project->projectDirectory())) { const FilePath dirInProject = parentDir.relativeChildPath(project->projectDirectory()); return "(" + dirInProject.toUserOutput() + " @ " + project->displayName() + ")"; } // For a file that is "outside" the project it belongs to, we display its // dir's full path because it is easier to read than a series of "../../.". // Example: /home/hugo/GenericProject/App.files lists /home/hugo/lib/Bar.cpp return "(" + parentDir.toUserOutput() + " @ " + project->displayName() + ")"; } QString ProjectManagerPrivate::windowTitleAddition(const FilePath &filePath) { return filePath.isEmpty() ? QString() : locationInProject(filePath); } FilePaths ProjectManagerPrivate::dependenciesOrder() const { QList> unordered; FilePaths ordered; // copy the map to a temporary list for (const Project *pro : m_projects) { const FilePath proName = pro->projectFilePath(); const FilePaths depList = filtered(m_depMap.value(proName), [this](const FilePath &proPath) { return contains(m_projects, [proPath](const Project *p) { return p->projectFilePath() == proPath; }); }); unordered.push_back({proName, depList}); } while (!unordered.isEmpty()) { for (int i = (unordered.count() - 1); i >= 0; --i) { if (unordered.at(i).second.isEmpty()) { ordered << unordered.at(i).first; unordered.removeAt(i); } } // remove the handled projects from the dependency lists // of the remaining unordered projects for (int i = 0; i < unordered.count(); ++i) { for (const FilePath &pro : std::as_const(ordered)) { FilePaths depList = unordered.at(i).second; depList.removeAll(pro); unordered[i].second = depList; } } } return ordered; } QList ProjectManager::projectOrder(const Project *project) { QList result; FilePaths pros; if (project) pros = d->dependencies(project->projectFilePath()); else pros = d->dependenciesOrder(); for (const FilePath &proFile : std::as_const(pros)) { for (Project *pro : projects()) { if (pro->projectFilePath() == proFile) { result << pro; break; } } } return result; } Project *ProjectManager::projectForFile(const FilePath &fileName) { if (Project * const project = Utils::findOrDefault(ProjectManager::projects(), [&fileName](const Project *p) { return p->isKnownFile(fileName); })) { return project; } return Utils::findOrDefault(ProjectManager::projects(), [&fileName](const Project *p) { return isInProjectSourceDir(fileName, *p); }); } bool ProjectManager::isInProjectSourceDir(const Utils::FilePath &filePath, const Project &project) { for (const Target * const target : project.targets()) { for (const BuildConfiguration * const bc : target->buildConfigurations()) { if (filePath.isChildOf(bc->buildDirectory())) return false; if (const FilePath canonicalBuildDir = bc->buildDirectory().canonicalPath(); canonicalBuildDir != bc->buildDirectory() && filePath.isChildOf(canonicalBuildDir)) { return false; } } } if (filePath.isChildOf(project.projectDirectory())) return true; if (const FilePath canonicalRoot = project.projectDirectory().canonicalPath(); canonicalRoot != project.projectDirectory()) { return filePath.isChildOf(canonicalRoot); } return false; } Project *ProjectManager::projectWithProjectFilePath(const FilePath &filePath) { return Utils::findOrDefault(ProjectManager::projects(), [&filePath](const Project *p) { return p->projectFilePath() == filePath; }); } void ProjectManager::configureEditor(IEditor *editor, const FilePath &filePath) { if (auto textEditor = qobject_cast(editor)) { // Global settings are the default. if (Project *project = projectForFile(filePath)) project->editorConfiguration()->configureEditor(textEditor); } } void ProjectManager::configureEditors(Project *project) { const QList documents = DocumentModel::openedDocuments(); for (IDocument *document : documents) { if (project->isKnownFile(document->filePath())) { const QList editors = DocumentModel::editorsForDocument(document); for (IEditor *editor : editors) { if (auto textEditor = qobject_cast(editor)) { project->editorConfiguration()->configureEditor(textEditor); } } } } } void ProjectManager::removeProjects(const QList &remove) { for (Project *pro : remove) emit m_instance->aboutToRemoveProject(pro); bool changeStartupProject = false; // Delete projects for (Project *pro : remove) { pro->saveSettings(); pro->markAsShuttingDown(); // Remove the project node: d->m_projects.removeOne(pro); if (pro == d->m_startupProject) changeStartupProject = true; FolderNavigationWidgetFactory::removeRootDirectory(projectFolderId(pro)); disconnect(pro, nullptr, m_instance, nullptr); emit m_instance->projectRemoved(pro); } if (changeStartupProject) setStartupProject(hasProjects() ? projects().first() : nullptr); qDeleteAll(remove); } void ProjectManagerPrivate::restoreDependencies() { QMap depMap = mapEntryFromStoreEntry(SessionManager::sessionValue( "ProjectDependencies")).toMap(); auto i = depMap.constBegin(); while (i != depMap.constEnd()) { const QString &key = i.key(); FilePaths values; const QStringList valueList = i.value().toStringList(); for (const QString &value : valueList) values << FilePath::fromString(value); m_depMap.insert(FilePath::fromString(key), values); ++i; } } void ProjectManagerPrivate::askUserAboutFailedProjects() { FilePaths failedProjects = m_failedProjects; if (!failedProjects.isEmpty()) { QString fileList = FilePath::formatFilePaths(failedProjects, "
"); QMessageBox box(QMessageBox::Warning, Tr::tr("Failed to restore project files"), Tr::tr("Could not restore the following project files:
%1"). arg(fileList)); auto keepButton = new QPushButton(Tr::tr("Keep projects in Session"), &box); auto removeButton = new QPushButton(Tr::tr("Remove projects from Session"), &box); box.addButton(keepButton, QMessageBox::AcceptRole); box.addButton(removeButton, QMessageBox::DestructiveRole); box.exec(); if (box.clickedButton() == removeButton) m_failedProjects.clear(); } } void ProjectManagerPrivate::restoreStartupProject() { const FilePath startupProject = FilePath::fromSettings( SessionManager::sessionValue("StartupProject")); if (!startupProject.isEmpty()) { for (Project *pro : std::as_const(m_projects)) { if (pro->projectFilePath() == startupProject) { m_instance->setStartupProject(pro); break; } } } if (!m_startupProject) { if (!startupProject.isEmpty()) qWarning() << "Could not find startup project" << startupProject; if (hasProjects()) m_instance->setStartupProject(m_projects.first()); } } /*! Loads a session, takes a session name (not filename). */ void ProjectManagerPrivate::restoreProjects(const FilePaths &fileList) { // indirectly adds projects to session // Keep projects that failed to load in the session! m_failedProjects = fileList; if (!fileList.isEmpty()) { OpenProjectResult result = ProjectExplorerPlugin::openProjects(fileList); if (!result) ProjectExplorerPlugin::showOpenProjectError(result); const QList projects = result.projects(); for (const Project *p : projects) m_failedProjects.removeAll(p->projectFilePath()); } } void ProjectManagerPrivate::loadSession() { d->m_failedProjects.clear(); d->m_depMap.clear(); d->m_casadeSetActive = false; // not ideal that this is in ProjectManager Id modeId = Id::fromSetting(SessionManager::value("ActiveMode")); if (!modeId.isValid()) modeId = Id(Core::Constants::MODE_EDIT); // find a list of projects to close later const FilePaths fileList = FileUtils::toFilePathList( SessionManager::sessionValue("ProjectList").toStringList()); const QList projectsToRemove = Utils::filtered(ProjectManager::projects(), [&fileList](Project *p) { return !fileList.contains(p->projectFilePath()); }); const QList openProjects = ProjectManager::projects(); const FilePaths projectPathsToLoad = Utils::filtered(fileList, [&openProjects](const FilePath &path) { return !Utils::contains(openProjects, [&path](Project *p) { return p->projectFilePath() == path; }); }); SessionManager::addSessionLoadingSteps(projectPathsToLoad.count()); d->restoreProjects(projectPathsToLoad); d->restoreDependencies(); d->restoreStartupProject(); // only remove old projects now that the startup project is set! ProjectManager::removeProjects(projectsToRemove); // Fall back to Project mode if the startup project is unconfigured and // use the mode saved in the session otherwise if (d->m_startupProject && d->m_startupProject->needsConfiguration()) modeId = Id(Constants::MODE_SESSION); ModeManager::activateMode(modeId); ModeManager::setFocusToCurrentMode(); d->m_casadeSetActive = SessionManager::sessionValue("CascadeSetActive", false).toBool(); // Starts a event loop, better do that at the very end QMetaObject::invokeMethod(m_instance, [this] { askUserAboutFailedProjects(); }); } FilePaths ProjectManager::projectsForSessionName(const QString &session) { const FilePath fileName = SessionManager::sessionNameToFileName(session); PersistentSettingsReader reader; if (fileName.exists()) { if (!reader.load(fileName)) { qWarning() << "Could not restore session" << fileName.toUserOutput(); return {}; } } return transform(reader.restoreValue("ProjectList").toStringList(), &FilePath::fromUserInput); } #ifdef WITH_TESTS void ProjectExplorerTest::testSessionSwitch() { QVERIFY(SessionManager::createSession("session1")); QVERIFY(SessionManager::createSession("session2")); QTemporaryFile cppFile("main.cpp"); QVERIFY(cppFile.open()); cppFile.close(); QTemporaryFile projectFile1("XXXXXX.pro"); QTemporaryFile projectFile2("XXXXXX.pro"); struct SessionSpec { SessionSpec(const QString &n, QTemporaryFile &f) : name(n), projectFile(f) {} const QString name; QTemporaryFile &projectFile; }; std::vector sessionSpecs{SessionSpec("session1", projectFile1), SessionSpec("session2", projectFile2)}; for (const SessionSpec &sessionSpec : sessionSpecs) { static const QByteArray proFileContents = "TEMPLATE = app\n" "CONFIG -= qt\n" "SOURCES = " + cppFile.fileName().toLocal8Bit(); QVERIFY(sessionSpec.projectFile.open()); sessionSpec.projectFile.write(proFileContents); sessionSpec.projectFile.close(); QVERIFY(SessionManager::loadSession(sessionSpec.name)); const OpenProjectResult openResult = ProjectExplorerPlugin::openProject( FilePath::fromString(sessionSpec.projectFile.fileName())); if (openResult.errorMessage().contains("text/plain")) QSKIP("This test requires the presence of QmakeProjectManager to be fully functional. " "Hint: run this test with \"-load QmakeProjectManager\" option."); QVERIFY(openResult); QCOMPARE(openResult.projects().count(), 1); QVERIFY(openResult.project()); QCOMPARE(ProjectManager::projects().count(), 1); } for (int i = 0; i < 30; ++i) { QVERIFY(SessionManager::loadSession("session1")); QCOMPARE(SessionManager::activeSession(), "session1"); QCOMPARE(ProjectManager::projects().count(), 1); QVERIFY(SessionManager::loadSession("session2")); QCOMPARE(SessionManager::activeSession(), "session2"); QCOMPARE(ProjectManager::projects().count(), 1); } QVERIFY(SessionManager::loadSession("session1")); ProjectManager::closeAllProjects(); QVERIFY(SessionManager::loadSession("session2")); ProjectManager::closeAllProjects(); QVERIFY(SessionManager::deleteSession("session1")); QVERIFY(SessionManager::deleteSession("session2")); } #endif // WITH_TESTS } // namespace ProjectExplorer