diff --git a/src/plugins/projectexplorer/CMakeLists.txt b/src/plugins/projectexplorer/CMakeLists.txt index b449aababf4..0c62d2177a1 100644 --- a/src/plugins/projectexplorer/CMakeLists.txt +++ b/src/plugins/projectexplorer/CMakeLists.txt @@ -179,6 +179,7 @@ add_qtc_plugin(ProjectExplorer vcsannotatetaskhandler.cpp vcsannotatetaskhandler.h waitforstopdialog.cpp waitforstopdialog.h windebuginterface.cpp windebuginterface.h + workspaceproject.h workspaceproject.cpp xcodebuildparser.cpp xcodebuildparser.h ) diff --git a/src/plugins/projectexplorer/ProjectExplorer.json.in b/src/plugins/projectexplorer/ProjectExplorer.json.in index b5f30d4242c..0b418f5d145 100644 --- a/src/plugins/projectexplorer/ProjectExplorer.json.in +++ b/src/plugins/projectexplorer/ProjectExplorer.json.in @@ -37,6 +37,13 @@ " ", " ", " ", + + " ", + " ", + " Workspace Qt Creator Project file", + " ", + " ", + "" ] } diff --git a/src/plugins/projectexplorer/project.cpp b/src/plugins/projectexplorer/project.cpp index 7d0ed75c6c6..a46f5d5eefc 100644 --- a/src/plugins/projectexplorer/project.cpp +++ b/src/plugins/projectexplorer/project.cpp @@ -1252,6 +1252,14 @@ void Project::addVariablesToMacroExpander(const QByteArray &prefix, return project->projectFilePath(); return {}; }); + expander->registerVariable(fullPrefix + "ProjectDirectory", + //: %1 is something like "Active project" + ::PE::Tr::tr("%1: Full path to Project Directory.").arg(descriptor), + [projectGetter]() -> QString { + if (const Project *const project = projectGetter()) + return project->projectDirectory().toUserOutput(); + return {}; + }); expander->registerVariable(fullPrefix + "Kit:Name", //: %1 is something like "Active project" ::PE::Tr::tr("%1: The name of the active kit.").arg(descriptor), diff --git a/src/plugins/projectexplorer/project.h b/src/plugins/projectexplorer/project.h index 9906047bc21..ec6c341bb2f 100644 --- a/src/plugins/projectexplorer/project.h +++ b/src/plugins/projectexplorer/project.h @@ -68,8 +68,8 @@ public: BuildSystem *createBuildSystem(Target *target) const; - Utils::FilePath projectFilePath() const; - Utils::FilePath projectDirectory() const; + virtual Utils::FilePath projectFilePath() const; + virtual Utils::FilePath projectDirectory() const; // This does not affect nodes, only the root path. void changeRootProjectDirectory(); diff --git a/src/plugins/projectexplorer/projectexplorer.cpp b/src/plugins/projectexplorer/projectexplorer.cpp index dd4d6861686..336f1d22461 100644 --- a/src/plugins/projectexplorer/projectexplorer.cpp +++ b/src/plugins/projectexplorer/projectexplorer.cpp @@ -80,6 +80,7 @@ #include "toolchainmanager.h" #include "toolchainoptionspage.h" #include "vcsannotatetaskhandler.h" +#include "workspaceproject.h" #ifdef Q_OS_WIN #include "windebuginterface.h" @@ -197,6 +198,7 @@ const int P_MODE_SESSION = 85; // Actions const char LOAD[] = "ProjectExplorer.Load"; +const char LOADWORKSPACE[] = "ProjectExplorer.LoadWorkspace"; const char UNLOAD[] = "ProjectExplorer.Unload"; const char UNLOADCM[] = "ProjectExplorer.UnloadCM"; const char UNLOADOTHERSCM[] = "ProjectExplorer.UnloadOthersCM"; @@ -479,6 +481,7 @@ public: void buildQueueFinished(bool success); void loadAction(); + void openWorkspaceAction(); void handleUnloadProject(); void unloadProjectContextMenu(); void unloadOtherProjectsContextMenu(); @@ -543,6 +546,7 @@ public: QAction *m_newAction; QAction *m_loadAction; + QAction *m_loadWorkspaceAction; Action *m_unloadAction; Action *m_unloadActionContextMenu; Action *m_unloadOthersActionContextMenu; @@ -1099,6 +1103,12 @@ bool ProjectExplorerPlugin::initialize(const QStringList &arguments, QString *er cmd->setDefaultKeySequence(QKeySequence(Tr::tr("Ctrl+Shift+O"))); msessionContextMenu->addAction(cmd, Constants::G_SESSION_FILES); + // load workspace action + dd->m_loadWorkspaceAction = new QAction(Tr::tr("Open Workspace..."), this); + cmd = ActionManager::registerAction(dd->m_loadWorkspaceAction, Constants::LOADWORKSPACE); + mfile->addAction(cmd, Core::Constants::G_FILE_OPEN); + msessionContextMenu->addAction(cmd, Constants::G_SESSION_FILES); + // Default open action dd->m_openFileAction = new QAction(Tr::tr("Open File"), this); cmd = ActionManager::registerAction(dd->m_openFileAction, Constants::OPENFILE, @@ -1659,6 +1669,8 @@ bool ProjectExplorerPlugin::initialize(const QStringList &arguments, QString *er dd, &ProjectExplorerPlugin::openNewProjectDialog); connect(dd->m_loadAction, &QAction::triggered, dd, &ProjectExplorerPluginPrivate::loadAction); + connect(dd->m_loadWorkspaceAction, &QAction::triggered, + dd, &ProjectExplorerPluginPrivate::openWorkspaceAction); connect(dd->m_buildProjectOnlyAction, &QAction::triggered, dd, [] { BuildManager::buildProjectWithoutDependencies(ProjectManager::startupProject()); }); @@ -1887,6 +1899,8 @@ bool ProjectExplorerPlugin::initialize(const QStringList &arguments, QString *er DeviceManager::instance()->addDevice(IDevice::Ptr(new DesktopDevice)); + setupWorkspaceProject(this); + #ifdef WITH_TESTS addTestCreator(&createSanitizerOutputParserTest); #endif @@ -1919,6 +1933,30 @@ void ProjectExplorerPluginPrivate::loadAction() updateActions(); } +void ProjectExplorerPluginPrivate::openWorkspaceAction() +{ + FilePath dir = dd->m_lastOpenDirectory; + + // for your special convenience, we preselect a pro file if it is + // the current file + if (const IDocument *document = EditorManager::currentDocument()) { + const FilePath fn = document->filePath(); + const bool isProject = dd->m_profileMimeTypes.contains(document->mimeType()); + dir = isProject ? fn : fn.absolutePath(); + } + + FilePath filePath = Utils::FileUtils::getExistingDirectory( + ICore::dialogParent(), Tr::tr("Open Workspace"), dir); + if (filePath.isEmpty()) + return; + + OpenProjectResult result = ProjectExplorerPlugin::openProject(filePath); + if (!result) + ProjectExplorerPlugin::showOpenProjectError(result); + + updateActions(); +} + void ProjectExplorerPluginPrivate::unloadProjectContextMenu() { if (Project *p = ProjectTree::currentProject()) @@ -2282,10 +2320,7 @@ OpenProjectResult ProjectExplorerPlugin::openProjects(const FilePaths &filePaths MimeType mt = Utils::mimeTypeForFile(filePath); if (ProjectManager::canOpenProjectForMimeType(mt)) { - if (!filePath.isFile()) { - appendError(errorString, - Tr::tr("Failed opening project \"%1\": Project is not a file.").arg(filePath.toUserOutput())); - } else if (Project *pro = ProjectManager::openProject(mt, filePath)) { + if (Project *pro = ProjectManager::openProject(mt, filePath)) { QString restoreError; Project::RestoreResult restoreResult = pro->restoreSettings(&restoreError); if (restoreResult == Project::RestoreResult::Ok) { diff --git a/src/plugins/projectexplorer/workspaceproject.cpp b/src/plugins/projectexplorer/workspaceproject.cpp new file mode 100644 index 00000000000..d380f7cea88 --- /dev/null +++ b/src/plugins/projectexplorer/workspaceproject.cpp @@ -0,0 +1,210 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "workspaceproject.h" + +#include "buildsystem.h" +#include "projectexplorer.h" +#include "projectexplorerconstants.h" +#include "projectexplorertr.h" +#include "projectmanager.h" +#include "projecttree.h" +#include "target.h" +#include "treescanner.h" + +#include + +#include +#include + +#include +#include +#include + +const QLatin1StringView FOLDER_MIMETYPE{"inode/directory"}; +const QLatin1StringView WORKSPACE_MIMETYPE{"text/x-workspace-project"}; +const QLatin1StringView WORKSPACE_PROJECT_ID{"ProjectExplorer.WorkspaceProject"}; + +const QLatin1StringView PROJECT_NAME_KEY{"project.name"}; +const QLatin1StringView FILES_EXCLUDE_KEY{"files.exclude"}; +const QLatin1StringView EXCLUDE_ACTION_ID{"ProjectExplorer.ExcludeFromWorkspace"}; + + +using namespace Utils; +using namespace Core; + +namespace ProjectExplorer { + +const expected_str projectDefinition(const Project *project) +{ + if (auto fileContents = project->projectFilePath().fileContents()) + return QJsonDocument::fromJson(*fileContents).object(); + return {}; +} + +static bool checkEnabled(FolderNode *fn) +{ + if (fn->findChildFileNode([](FileNode *fn) { return fn->isEnabled(); })) + return true; + + if (fn->findChildFolderNode([](FolderNode *fn) { return checkEnabled(fn); })) + return true; + + fn->setEnabled(false); + return false; +} + +class WorkspaceBuildSystem : public BuildSystem +{ +public: + WorkspaceBuildSystem(Target *t) + :BuildSystem(t) + { + connect(&m_scanner, &TreeScanner::finished, this, [this] { + auto root = std::make_unique(projectDirectory()); + root->setDisplayName(target()->project()->displayName()); + std::vector> nodePtrs + = Utils::transform(m_scanner.release().allFiles, [this](FileNode *fn) { + fn->setEnabled(!Utils::anyOf( + m_filters, [path = fn->path().path()](const QRegularExpression &filter) { + return filter.match(path).hasMatch(); + })); + return std::unique_ptr(fn); + }); + root->addNestedNodes(std::move(nodePtrs)); + root->forEachFolderNode(&checkEnabled); + setRootProjectNode(std::move(root)); + + m_parseGuard.markAsSuccess(); + m_parseGuard = {}; + + emitBuildSystemUpdated(); + }); + m_scanner.setDirFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::Hidden); + + connect(target()->project(), + &Project::projectFileIsDirty, + this, + &BuildSystem::requestDelayedParse); + + requestDelayedParse(); + } + + void triggerParsing() final + { + m_filters.clear(); + FilePath projectPath = project()->projectDirectory(); + + const QJsonObject json = projectDefinition(project()).value_or(QJsonObject()); + const QJsonValue projectNameValue = json.value(PROJECT_NAME_KEY); + if (projectNameValue.isString()) + project()->setDisplayName(projectNameValue.toString()); + const QJsonArray excludesJson = json.value(FILES_EXCLUDE_KEY).toArray(); + for (QJsonValue excludeJson : excludesJson) { + if (excludeJson.isString()) { + FilePath absolute = projectPath.pathAppended(excludeJson.toString()); + if (absolute.isDir()) + absolute = absolute.pathAppended("*"); + m_filters << QRegularExpression( + Utils::wildcardToRegularExpression(absolute.path()), + QRegularExpression::CaseInsensitiveOption); + } + } + + m_parseGuard = guardParsingRun(); + m_scanner.asyncScanForFiles(target()->project()->projectDirectory()); + } + + QString name() const final { return QLatin1String("Workspace"); } + +private: + QList m_filters; + ParseGuard m_parseGuard; + TreeScanner m_scanner; +}; + +class WorkspaceProject : public Project +{ + Q_OBJECT +public: + WorkspaceProject(const FilePath file) + : Project(FOLDER_MIMETYPE, file.isDir() ? file / ".qtcreator" / "project.json" : file) + { + QTC_CHECK(projectFilePath().absolutePath().ensureWritableDir()); + QTC_CHECK(projectFilePath().ensureExistingFile()); + + setId(Id::fromString(WORKSPACE_PROJECT_ID)); + setDisplayName(projectDirectory().fileName()); + setBuildSystemCreator([](Target *t) { return new WorkspaceBuildSystem(t); }); + } + + FilePath projectDirectory() const override + { + return Project::projectDirectory().parentDir(); + } + + void saveProjectDefinition(const QJsonObject &json) + { + Utils::FileSaver saver(projectFilePath()); + saver.write(QJsonDocument(json).toJson()); + saver.finalize(); + } + + void excludePath(const FilePath &path) + { + QTC_ASSERT(projectFilePath().exists(), return); + if (expected_str json = projectDefinition(this)) { + QJsonArray excludes = (*json)[FILES_EXCLUDE_KEY].toArray(); + const QString relative = path.relativePathFrom(projectDirectory()).path(); + if (excludes.contains(relative)) + return; + excludes << relative; + json->insert(FILES_EXCLUDE_KEY, excludes); + saveProjectDefinition(*json); + } + } + + void excludeNode(Node *node) + { + node->setEnabled(false); + if (auto fileNode = node->asFileNode()) { + excludePath(fileNode->path()); + } else if (auto folderNode = node->asFolderNode()) { + folderNode->forEachNode([](Node *node) { node->setEnabled(false); }); + excludePath(folderNode->path()); + } + } +}; + +void setupWorkspaceProject(QObject *guard) +{ + ProjectManager::registerProjectType(FOLDER_MIMETYPE); + ProjectManager::registerProjectType(WORKSPACE_MIMETYPE); + + QObject::connect( + ProjectTree::instance(), + &ProjectTree::aboutToShowContextMenu, + ProjectExplorerPlugin::instance(), + [](Node *node) { + const bool enabled = node && node->isEnabled() + && qobject_cast(node->getProject()); + ActionManager::command(Id::fromString(EXCLUDE_ACTION_ID))->action()->setEnabled(enabled); + }); + + ActionBuilder excludeAction(guard, Id::fromString(EXCLUDE_ACTION_ID)); + excludeAction.setContext(Id::fromString(WORKSPACE_PROJECT_ID)); + excludeAction.setText(Tr::tr("Exclude from Project")); + excludeAction.addToContainer(Constants::M_FOLDERCONTEXT, Constants::G_FOLDER_OTHER); + excludeAction.addToContainer(Constants::M_FILECONTEXT, Constants::G_FILE_OTHER); + excludeAction.addOnTriggered([] { + Node *node = ProjectTree::currentNode(); + QTC_ASSERT(node, return); + const auto project = qobject_cast(node->getProject()); + QTC_ASSERT(project, return); + project->excludeNode(node); + }); +} + +} // namespace ProjectExplorer + +#include "workspaceproject.moc" diff --git a/src/plugins/projectexplorer/workspaceproject.h b/src/plugins/projectexplorer/workspaceproject.h new file mode 100644 index 00000000000..3fe26a8c8e6 --- /dev/null +++ b/src/plugins/projectexplorer/workspaceproject.h @@ -0,0 +1,16 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +QT_BEGIN_NAMESPACE +class QObject; +QT_END_NAMESPACE + +namespace ProjectExplorer { + +void setupWorkspaceProject(QObject *guard); + +} // namespace ProjectExplorer