forked from qt-creator/qt-creator
ProjectExplorer: add watcher to WorkspaceProject directories
Change-Id: I6335515239e31239a32ca8c3e0155f161103aeab Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
This commit is contained in:
@@ -18,8 +18,11 @@
|
|||||||
#include "treescanner.h"
|
#include "treescanner.h"
|
||||||
|
|
||||||
#include <coreplugin/actionmanager/actionmanager.h>
|
#include <coreplugin/actionmanager/actionmanager.h>
|
||||||
|
#include <coreplugin/iversioncontrol.h>
|
||||||
|
#include <coreplugin/vcsmanager.h>
|
||||||
|
|
||||||
#include <utils/algorithm.h>
|
#include <utils/algorithm.h>
|
||||||
|
#include <utils/filesystemwatcher.h>
|
||||||
#include <utils/fileutils.h>
|
#include <utils/fileutils.h>
|
||||||
#include <utils/stringutils.h>
|
#include <utils/stringutils.h>
|
||||||
|
|
||||||
@@ -32,6 +35,8 @@ using namespace Core;
|
|||||||
|
|
||||||
namespace ProjectExplorer {
|
namespace ProjectExplorer {
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(wsbs, "qtc.projectexplorer.workspacebuildsystem", QtWarningMsg);
|
||||||
|
|
||||||
const QLatin1StringView FOLDER_MIMETYPE{"inode/directory"};
|
const QLatin1StringView FOLDER_MIMETYPE{"inode/directory"};
|
||||||
const QLatin1StringView WORKSPACE_MIMETYPE{"text/x-workspace-project"};
|
const QLatin1StringView WORKSPACE_MIMETYPE{"text/x-workspace-project"};
|
||||||
const char WORKSPACE_PROJECT_ID[] = "ProjectExplorer.WorkspaceProject";
|
const char WORKSPACE_PROJECT_ID[] = "ProjectExplorer.WorkspaceProject";
|
||||||
@@ -49,113 +54,264 @@ const expected_str<QJsonObject> projectDefinition(const Project *project)
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static QFlags<QDir::Filter> workspaceDirFilter = QDir::AllEntries | QDir::NoDotAndDotDot | QDir::Hidden;
|
||||||
|
|
||||||
class WorkspaceBuildSystem final : public BuildSystem
|
class WorkspaceBuildSystem final : public BuildSystem
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
WorkspaceBuildSystem(Target *t)
|
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, [](FileNode *fn) {
|
|
||||||
return std::unique_ptr<FileNode>(fn);
|
|
||||||
});
|
|
||||||
root->addNestedNodes(std::move(nodePtrs));
|
|
||||||
setRootProjectNode(std::move(root));
|
|
||||||
|
|
||||||
m_parseGuard.markAsSuccess();
|
void triggerParsing() final;
|
||||||
m_parseGuard = {};
|
|
||||||
|
|
||||||
emitBuildSystemUpdated();
|
void watchFolder(const FilePath &path, const QList<IVersionControl *> &versionControls);
|
||||||
});
|
|
||||||
m_scanner.setDirFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::Hidden);
|
|
||||||
m_scanner.setFilter([&](const Utils::MimeType &, const Utils::FilePath &filePath) {
|
|
||||||
return Utils::anyOf(m_filters, [filePath](const QRegularExpression &filter) {
|
|
||||||
return filter.match(filePath.path()).hasMatch();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(target()->project(),
|
void handleDirectoryChanged(const FilePath &directory);
|
||||||
&Project::projectFileIsDirty,
|
|
||||||
this,
|
|
||||||
&BuildSystem::requestDelayedParse);
|
|
||||||
|
|
||||||
requestDelayedParse();
|
void scan(const FilePath &path);
|
||||||
}
|
void scanNext();
|
||||||
|
|
||||||
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 (const QJsonValue &excludeJson : excludesJson) {
|
|
||||||
if (excludeJson.isString()) {
|
|
||||||
FilePath absolute = projectPath.pathAppended(excludeJson.toString());
|
|
||||||
m_filters << QRegularExpression(
|
|
||||||
Utils::wildcardToRegularExpression(absolute.path()),
|
|
||||||
QRegularExpression::CaseInsensitiveOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
m_parseGuard = guardParsingRun();
|
|
||||||
m_scanner.asyncScanForFiles(target()->project()->projectDirectory());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString name() const final { return QLatin1String("Workspace"); }
|
QString name() const final { return QLatin1String("Workspace"); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool isFiltered(const FilePath &path, QList<IVersionControl *> versionControls) const;
|
||||||
|
|
||||||
QList<QRegularExpression> m_filters;
|
QList<QRegularExpression> m_filters;
|
||||||
|
std::unique_ptr<FileSystemWatcher> m_watcher;
|
||||||
ParseGuard m_parseGuard;
|
ParseGuard m_parseGuard;
|
||||||
|
FilePaths m_scanQueue;
|
||||||
TreeScanner m_scanner;
|
TreeScanner m_scanner;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static FolderNode *findAvailableParent(ProjectNode *root, const FilePath &path)
|
||||||
|
{
|
||||||
|
FolderNode *result = nullptr;
|
||||||
|
FolderNode *child = root;
|
||||||
|
do {
|
||||||
|
result = child;
|
||||||
|
child = result->findChildFolderNode(
|
||||||
|
[&path](FolderNode *fn) { return path.isChildOf(fn->path()) || path == fn->path(); });
|
||||||
|
} while (child);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceBuildSystem::WorkspaceBuildSystem(Target *t)
|
||||||
|
:BuildSystem(t)
|
||||||
|
{
|
||||||
|
connect(&m_scanner, &TreeScanner::finished, this, [this] {
|
||||||
|
QTC_ASSERT(!m_scanQueue.isEmpty(), return);
|
||||||
|
const FilePath scannedDir = m_scanQueue.takeFirst();
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<FileNode>> nodePtrs
|
||||||
|
= Utils::transform<std::vector>(m_scanner.release().allFiles, [](FileNode *fn) {
|
||||||
|
return std::unique_ptr<FileNode>(fn);
|
||||||
|
});
|
||||||
|
if (scannedDir == projectDirectory()) {
|
||||||
|
qCDebug(wsbs) << "Finished scanning new root" << scannedDir << "found"
|
||||||
|
<< nodePtrs.size() << "file entries";
|
||||||
|
auto root = std::make_unique<ProjectNode>(scannedDir);
|
||||||
|
root->setDisplayName(target()->project()->displayName());
|
||||||
|
m_watcher.reset(new FileSystemWatcher);
|
||||||
|
connect(
|
||||||
|
m_watcher.get(),
|
||||||
|
&FileSystemWatcher::directoryChanged,
|
||||||
|
this,
|
||||||
|
[this](const QString &path) {
|
||||||
|
handleDirectoryChanged(Utils::FilePath::fromPathPart(path));
|
||||||
|
});
|
||||||
|
|
||||||
|
root->addNestedNodes(std::move(nodePtrs));
|
||||||
|
setRootProjectNode(std::move(root));
|
||||||
|
} else {
|
||||||
|
qCDebug(wsbs) << "Finished scanning subdir" << scannedDir << "found"
|
||||||
|
<< nodePtrs.size() << "file entries";
|
||||||
|
FolderNode *parent = findAvailableParent(project()->rootProjectNode(), scannedDir);
|
||||||
|
const FilePath relativePath = scannedDir.relativeChildPath(parent->filePath());
|
||||||
|
const QString firstRelativeFolder = relativePath.path().left(relativePath.path().indexOf('/'));
|
||||||
|
const FilePath nodePath = parent->filePath() / firstRelativeFolder;
|
||||||
|
auto newNode = std::make_unique<FolderNode>(nodePath);
|
||||||
|
newNode->setDisplayName(firstRelativeFolder);
|
||||||
|
newNode->addNestedNodes(std::move(nodePtrs));
|
||||||
|
parent->replaceSubtree(nullptr, std::move(newNode));
|
||||||
|
}
|
||||||
|
watchFolder(scannedDir, VcsManager::versionControls());
|
||||||
|
|
||||||
|
scanNext();
|
||||||
|
});
|
||||||
|
m_scanner.setDirFilter(workspaceDirFilter);
|
||||||
|
m_scanner.setFilter([&](const Utils::MimeType &, const Utils::FilePath &filePath) {
|
||||||
|
return Utils::anyOf(m_filters, [filePath](const QRegularExpression &filter) {
|
||||||
|
return filter.match(filePath.path()).hasMatch();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(target()->project(),
|
||||||
|
&Project::projectFileIsDirty,
|
||||||
|
this,
|
||||||
|
&BuildSystem::requestDelayedParse);
|
||||||
|
|
||||||
|
requestDelayedParse();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WorkspaceBuildSystem::triggerParsing()
|
||||||
|
{
|
||||||
|
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 (const QJsonValue &excludeJson : excludesJson) {
|
||||||
|
if (excludeJson.isString()) {
|
||||||
|
FilePath absolute = projectPath.pathAppended(excludeJson.toString());
|
||||||
|
m_filters << QRegularExpression(
|
||||||
|
Utils::wildcardToRegularExpression(absolute.path()),
|
||||||
|
QRegularExpression::CaseInsensitiveOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
scan(target()->project()->projectDirectory());
|
||||||
|
}
|
||||||
|
|
||||||
|
void WorkspaceBuildSystem::watchFolder(
|
||||||
|
const FilePath &path, const QList<IVersionControl *> &versionControls)
|
||||||
|
{
|
||||||
|
if (!m_watcher->watchesDirectory(path)) {
|
||||||
|
qCDebug(wsbs) << "Adding watch for " << path;
|
||||||
|
m_watcher->addDirectory(path, FileSystemWatcher::WatchAllChanges);
|
||||||
|
}
|
||||||
|
for (auto entry : path.dirEntries(QDir::NoDotAndDotDot | QDir::Hidden | QDir::Dirs)) {
|
||||||
|
if (!isFiltered(entry, versionControls))
|
||||||
|
watchFolder(entry, versionControls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WorkspaceBuildSystem::handleDirectoryChanged(const FilePath &directory)
|
||||||
|
{
|
||||||
|
ProjectNode *root = project()->rootProjectNode();
|
||||||
|
QTC_ASSERT(root, return);
|
||||||
|
auto node = root->findNode(
|
||||||
|
[&directory](Node *node) {
|
||||||
|
if (node->asFolderNode()) {
|
||||||
|
qCDebug(wsbs) << "comparing" << node->filePath() << directory;
|
||||||
|
return node->filePath() == directory;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (!directory.exists()) {
|
||||||
|
m_watcher->removeDirectory(directory);
|
||||||
|
if (node)
|
||||||
|
node->parentFolderNode()->replaceSubtree(node, nullptr);
|
||||||
|
} else if (node) {
|
||||||
|
FolderNode *fn = node->asFolderNode();
|
||||||
|
QTC_ASSERT(fn, return);
|
||||||
|
const FilePaths entries = directory.dirEntries(workspaceDirFilter);
|
||||||
|
const QList<IVersionControl *> &versionControls = VcsManager::versionControls();
|
||||||
|
for (auto entry : entries) {
|
||||||
|
if (isFiltered(entry, versionControls))
|
||||||
|
continue;
|
||||||
|
if (entry.isDir()) {
|
||||||
|
if (!fn->folderNode(entry))
|
||||||
|
scan(entry);
|
||||||
|
} else if (!fn->fileNode(entry)) {
|
||||||
|
fn->replaceSubtree(
|
||||||
|
nullptr, std::make_unique<FileNode>(entry, Node::fileTypeForFileName(entry)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QList<Node *> toRemove;
|
||||||
|
auto filter = [&entries, &toRemove](Node *n) {
|
||||||
|
if (!entries.contains(n->filePath()))
|
||||||
|
toRemove << n;
|
||||||
|
};
|
||||||
|
fn->forEachFileNode(filter);
|
||||||
|
fn->forEachFolderNode(filter);
|
||||||
|
for (auto n : toRemove)
|
||||||
|
fn->replaceSubtree(n, nullptr);
|
||||||
|
} else {
|
||||||
|
scan(directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WorkspaceBuildSystem::scan(const FilePath &path)
|
||||||
|
{
|
||||||
|
m_scanQueue << path;
|
||||||
|
scanNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WorkspaceBuildSystem::scanNext()
|
||||||
|
{
|
||||||
|
if (m_scanQueue.isEmpty()) {
|
||||||
|
qCDebug(wsbs) << "Scan done.";
|
||||||
|
m_parseGuard.markAsSuccess();
|
||||||
|
m_parseGuard = {};
|
||||||
|
|
||||||
|
emitBuildSystemUpdated();
|
||||||
|
} else {
|
||||||
|
if (!m_parseGuard.guardsProject())
|
||||||
|
m_parseGuard = guardParsingRun();
|
||||||
|
if (m_scanner.isFinished()) {
|
||||||
|
const FilePath next = m_scanQueue.first();
|
||||||
|
qCDebug(wsbs) << "Start scanning" << next;
|
||||||
|
m_scanner.asyncScanForFiles(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WorkspaceBuildSystem::isFiltered(const FilePath &path, QList<IVersionControl *> versionControls) const
|
||||||
|
{
|
||||||
|
const bool explicitlyExcluded = Utils::anyOf(
|
||||||
|
m_filters,
|
||||||
|
[path](const QRegularExpression &filter) {
|
||||||
|
return filter.match(path.path()).hasMatch();
|
||||||
|
});
|
||||||
|
if (explicitlyExcluded)
|
||||||
|
return true;
|
||||||
|
return Utils::anyOf(versionControls, [path](const IVersionControl *vc) {
|
||||||
|
return vc->isVcsFileOrDirectory(path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class WorkspaceRunConfiguration : public RunConfiguration
|
class WorkspaceRunConfiguration : public RunConfiguration
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
Reference in New Issue
Block a user