2024-05-02 14:40:37 +02:00
|
|
|
// 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"
|
2024-05-31 16:09:02 +02:00
|
|
|
#include "runconfiguration.h"
|
|
|
|
|
#include "runconfigurationaspects.h"
|
|
|
|
|
#include "runcontrol.h"
|
2024-05-02 14:40:37 +02:00
|
|
|
#include "target.h"
|
|
|
|
|
#include "treescanner.h"
|
|
|
|
|
|
|
|
|
|
#include <coreplugin/actionmanager/actionmanager.h>
|
|
|
|
|
|
|
|
|
|
#include <utils/algorithm.h>
|
|
|
|
|
#include <utils/stringutils.h>
|
|
|
|
|
|
|
|
|
|
#include <QJsonArray>
|
|
|
|
|
#include <QJsonDocument>
|
|
|
|
|
#include <QJsonObject>
|
|
|
|
|
|
|
|
|
|
const QLatin1StringView FOLDER_MIMETYPE{"inode/directory"};
|
|
|
|
|
const QLatin1StringView WORKSPACE_MIMETYPE{"text/x-workspace-project"};
|
|
|
|
|
const QLatin1StringView WORKSPACE_PROJECT_ID{"ProjectExplorer.WorkspaceProject"};
|
2024-05-31 16:09:02 +02:00
|
|
|
const QLatin1StringView WORKSPACE_PROJECT_RUNCONFIG_ID{"WorkspaceProject.RunConfiguration:"};
|
2024-05-02 14:40:37 +02:00
|
|
|
|
|
|
|
|
const QLatin1StringView PROJECT_NAME_KEY{"project.name"};
|
|
|
|
|
const QLatin1StringView FILES_EXCLUDE_KEY{"files.exclude"};
|
|
|
|
|
const QLatin1StringView EXCLUDE_ACTION_ID{"ProjectExplorer.ExcludeFromWorkspace"};
|
2024-06-20 15:14:41 +02:00
|
|
|
const QLatin1StringView RESCAN_ACTION_ID{"ProjectExplorer.RescanWorkspace"};
|
2024-05-02 14:40:37 +02:00
|
|
|
|
|
|
|
|
using namespace Utils;
|
|
|
|
|
using namespace Core;
|
|
|
|
|
|
|
|
|
|
namespace ProjectExplorer {
|
|
|
|
|
|
|
|
|
|
const expected_str<QJsonObject> 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<ProjectNode>(projectDirectory());
|
|
|
|
|
root->setDisplayName(target()->project()->displayName());
|
|
|
|
|
std::vector<std::unique_ptr<FileNode>> nodePtrs
|
|
|
|
|
= Utils::transform<std::vector>(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<FileNode>(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();
|
2024-05-31 16:09:02 +02:00
|
|
|
for (const QJsonValue &excludeJson : excludesJson) {
|
2024-05-02 14:40:37 +02:00
|
|
|
if (excludeJson.isString()) {
|
|
|
|
|
FilePath absolute = projectPath.pathAppended(excludeJson.toString());
|
|
|
|
|
if (absolute.isDir())
|
|
|
|
|
absolute = absolute.pathAppended("*");
|
|
|
|
|
m_filters << QRegularExpression(
|
|
|
|
|
Utils::wildcardToRegularExpression(absolute.path()),
|
|
|
|
|
QRegularExpression::CaseInsensitiveOption);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-31 16:09:02 +02:00
|
|
|
QList<BuildTargetInfo> targetInfos;
|
|
|
|
|
|
|
|
|
|
const QJsonArray targets = json.value("targets").toArray();
|
|
|
|
|
int i = 0;
|
|
|
|
|
for (const QJsonValue &target : targets) {
|
|
|
|
|
i++;
|
|
|
|
|
QTC_ASSERT(target.isObject(), continue);
|
|
|
|
|
const QJsonObject targetObject = target.toObject();
|
|
|
|
|
|
|
|
|
|
QJsonArray args = targetObject["arguments"].toArray();
|
|
|
|
|
QStringList arguments = Utils::transform<QStringList>(args, [](const QJsonValue &arg) {
|
|
|
|
|
return arg.toString();
|
|
|
|
|
});
|
|
|
|
|
FilePath workingDirectory = FilePath::fromUserInput(
|
|
|
|
|
targetObject["workingDirectory"].toString());
|
|
|
|
|
|
|
|
|
|
if (!workingDirectory.isDir())
|
|
|
|
|
workingDirectory = FilePath::currentWorkingPath();
|
|
|
|
|
|
|
|
|
|
const QString name = targetObject["name"].toString();
|
|
|
|
|
const FilePath executable = FilePath::fromUserInput(
|
|
|
|
|
targetObject["executable"].toString());
|
|
|
|
|
|
|
|
|
|
if (name.isEmpty() || executable.isEmpty())
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
BuildTargetInfo bti;
|
|
|
|
|
bti.buildKey = name + QString::number(i);
|
|
|
|
|
bti.displayName = name;
|
|
|
|
|
bti.displayNameUniquifier = QString(" (%1)").arg(i);
|
|
|
|
|
bti.targetFilePath = executable;
|
|
|
|
|
bti.projectFilePath = projectPath;
|
|
|
|
|
bti.workingDirectory = workingDirectory;
|
|
|
|
|
bti.additionalData = QVariantMap{{"arguments", arguments}};
|
|
|
|
|
|
|
|
|
|
targetInfos << bti;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setApplicationTargets(targetInfos);
|
|
|
|
|
|
2024-05-02 14:40:37 +02:00
|
|
|
m_parseGuard = guardParsingRun();
|
|
|
|
|
m_scanner.asyncScanForFiles(target()->project()->projectDirectory());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString name() const final { return QLatin1String("Workspace"); }
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
QList<QRegularExpression> m_filters;
|
|
|
|
|
ParseGuard m_parseGuard;
|
|
|
|
|
TreeScanner m_scanner;
|
|
|
|
|
};
|
|
|
|
|
|
2024-05-31 16:09:02 +02:00
|
|
|
class WorkspaceRunConfiguration : public RunConfiguration
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
WorkspaceRunConfiguration(Target *target, Id id)
|
|
|
|
|
: RunConfiguration(target, id)
|
|
|
|
|
{
|
|
|
|
|
hint.setText(
|
|
|
|
|
Tr::tr("You can edit this configuration inside the .qtcreator/project.json file."));
|
|
|
|
|
|
|
|
|
|
const BuildTargetInfo bti = buildTargetInfo();
|
|
|
|
|
executable.setLabelText(Tr::tr("Executable:"));
|
|
|
|
|
executable.setReadOnly(true);
|
|
|
|
|
executable.setValue(bti.targetFilePath);
|
|
|
|
|
|
|
|
|
|
auto argumentsAsString = [this]() {
|
|
|
|
|
return CommandLine{
|
|
|
|
|
"", buildTargetInfo().additionalData.toMap()["arguments"].toStringList()}
|
|
|
|
|
.arguments();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
arguments.setLabelText(Tr::tr("Arguments:"));
|
|
|
|
|
arguments.setReadOnly(true);
|
|
|
|
|
arguments.setMacroExpander(macroExpander());
|
|
|
|
|
arguments.setArguments(argumentsAsString());
|
|
|
|
|
|
|
|
|
|
workingDirectory.setLabelText(Tr::tr("Working directory:"));
|
|
|
|
|
workingDirectory.setReadOnly(true);
|
|
|
|
|
workingDirectory.setDefaultWorkingDirectory(bti.workingDirectory);
|
|
|
|
|
|
|
|
|
|
setCommandLineGetter([this] {
|
|
|
|
|
const BuildTargetInfo bti = buildTargetInfo();
|
|
|
|
|
CommandLine cmdLine{
|
|
|
|
|
macroExpander()->expand(bti.targetFilePath),
|
|
|
|
|
Utils::transform(
|
|
|
|
|
bti.additionalData.toMap()["arguments"].toStringList(),
|
|
|
|
|
[this](const QString &arg) { return macroExpander()->expand(arg); })};
|
|
|
|
|
|
|
|
|
|
return cmdLine;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setUpdater([this, argumentsAsString] {
|
|
|
|
|
const BuildTargetInfo bti = buildTargetInfo();
|
|
|
|
|
executable.setValue(bti.targetFilePath);
|
|
|
|
|
arguments.setArguments(argumentsAsString());
|
|
|
|
|
workingDirectory.setDefaultWorkingDirectory(bti.workingDirectory);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
connect(target, &Target::buildSystemUpdated, this, &RunConfiguration::update);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TextDisplay hint{this};
|
|
|
|
|
FilePathAspect executable{this};
|
|
|
|
|
ArgumentsAspect arguments{this};
|
|
|
|
|
WorkingDirectoryAspect workingDirectory{this};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class WorkspaceProjectRunConfigurationFactory : public RunConfigurationFactory
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
WorkspaceProjectRunConfigurationFactory()
|
|
|
|
|
{
|
|
|
|
|
registerRunConfiguration<WorkspaceRunConfiguration>(
|
|
|
|
|
Id::fromString(WORKSPACE_PROJECT_RUNCONFIG_ID));
|
|
|
|
|
addSupportedProjectType(Id::fromString(WORKSPACE_PROJECT_ID));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class WorkspaceProjectRunWorkerFactory : public RunWorkerFactory
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
WorkspaceProjectRunWorkerFactory()
|
|
|
|
|
{
|
|
|
|
|
setProduct<SimpleTargetRunner>();
|
|
|
|
|
addSupportedRunMode(Constants::NORMAL_RUN_MODE);
|
|
|
|
|
addSupportedRunConfig(Id::fromString(WORKSPACE_PROJECT_RUNCONFIG_ID));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-05-02 14:40:37 +02:00
|
|
|
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());
|
2024-06-12 10:34:43 +02:00
|
|
|
if (!projectFilePath().exists() && QTC_GUARD(projectFilePath().ensureExistingFile())) {
|
|
|
|
|
QJsonObject projectJson;
|
|
|
|
|
projectJson.insert("$schema", "https://download.qt.io/official_releases/qtcreator/latest/installer_source/jsonschemas/project.json");
|
|
|
|
|
projectJson.insert(FILES_EXCLUDE_KEY, QJsonArray{QJsonValue(".qtcreator/project.json.user")});
|
|
|
|
|
projectFilePath().writeFileContents(QJsonDocument(projectJson).toJson());
|
|
|
|
|
}
|
2024-05-02 14:40:37 +02:00
|
|
|
|
|
|
|
|
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<QJsonObject> 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<WorkspaceProject>(FOLDER_MIMETYPE);
|
|
|
|
|
ProjectManager::registerProjectType<WorkspaceProject>(WORKSPACE_MIMETYPE);
|
|
|
|
|
|
2024-06-20 15:14:41 +02:00
|
|
|
QAction *excludeAction = nullptr;
|
|
|
|
|
ActionBuilder(guard, Id::fromString(EXCLUDE_ACTION_ID))
|
|
|
|
|
.setContext(Id::fromString(WORKSPACE_PROJECT_ID))
|
|
|
|
|
.setText(Tr::tr("Exclude from Project"))
|
|
|
|
|
.addToContainer(Constants::M_FOLDERCONTEXT, Constants::G_FOLDER_OTHER)
|
|
|
|
|
.addToContainer(Constants::M_FILECONTEXT, Constants::G_FILE_OTHER)
|
|
|
|
|
.bindContextAction(&excludeAction)
|
|
|
|
|
.setCommandAttribute(Command::CA_Hide)
|
|
|
|
|
.addOnTriggered(guard, [] {
|
|
|
|
|
Node *node = ProjectTree::currentNode();
|
|
|
|
|
QTC_ASSERT(node, return);
|
|
|
|
|
const auto project = qobject_cast<WorkspaceProject *>(node->getProject());
|
|
|
|
|
QTC_ASSERT(project, return);
|
|
|
|
|
project->excludeNode(node);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
QAction *rescanAction = nullptr;
|
|
|
|
|
ActionBuilder(guard, Id::fromString(RESCAN_ACTION_ID))
|
|
|
|
|
.setContext(Id::fromString(WORKSPACE_PROJECT_ID))
|
|
|
|
|
.setText(Tr::tr("Rescan Workspace"))
|
|
|
|
|
.addToContainer(Constants::M_PROJECTCONTEXT, Constants::G_PROJECT_REBUILD)
|
|
|
|
|
.bindContextAction(&rescanAction)
|
|
|
|
|
.setCommandAttribute(Command::CA_Hide)
|
|
|
|
|
.addOnTriggered(guard, [] {
|
|
|
|
|
Node *node = ProjectTree::currentNode();
|
|
|
|
|
QTC_ASSERT(node, return);
|
|
|
|
|
const auto project = qobject_cast<WorkspaceProject *>(node->getProject());
|
|
|
|
|
QTC_ASSERT(project, return);
|
|
|
|
|
if (auto target = project->activeTarget()) {
|
|
|
|
|
if (target->buildSystem())
|
|
|
|
|
target->buildSystem()->triggerParsing();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2024-05-02 14:40:37 +02:00
|
|
|
QObject::connect(
|
|
|
|
|
ProjectTree::instance(),
|
|
|
|
|
&ProjectTree::aboutToShowContextMenu,
|
|
|
|
|
ProjectExplorerPlugin::instance(),
|
2024-06-20 15:14:41 +02:00
|
|
|
[excludeAction, rescanAction](Node *node) {
|
|
|
|
|
const bool visible = node && qobject_cast<WorkspaceProject *>(node->getProject());
|
|
|
|
|
excludeAction->setVisible(visible);
|
|
|
|
|
rescanAction->setVisible(visible);
|
|
|
|
|
if (visible) {
|
|
|
|
|
excludeAction->setEnabled(node->isEnabled());
|
|
|
|
|
bool enableRescan = false;
|
|
|
|
|
if (Project *project = node->getProject()) {
|
|
|
|
|
if (Target *target = project->activeTarget()) {
|
|
|
|
|
if (BuildSystem *buildSystem = target->buildSystem())
|
|
|
|
|
enableRescan = !buildSystem->isParsing();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
rescanAction->setEnabled(enableRescan);
|
|
|
|
|
}
|
2024-05-02 14:40:37 +02:00
|
|
|
});
|
|
|
|
|
|
2024-05-31 16:09:02 +02:00
|
|
|
static WorkspaceProjectRunConfigurationFactory theRunConfigurationFactory;
|
|
|
|
|
static WorkspaceProjectRunWorkerFactory theRunWorkerFactory;
|
2024-05-02 14:40:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace ProjectExplorer
|
|
|
|
|
|
|
|
|
|
#include "workspaceproject.moc"
|