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