From b76cfb1ea69f0ef57363098abab95aa566065411 Mon Sep 17 00:00:00 2001 From: David Schulz Date: Mon, 9 Sep 2024 15:43:57 +0200 Subject: [PATCH] ProjectExplorer: add watcher to WorkspaceProject directories Change-Id: I6335515239e31239a32ca8c3e0155f161103aeab Reviewed-by: Marcus Tillmanns --- .../projectexplorer/workspaceproject.cpp | 338 +++++++++++++----- 1 file changed, 247 insertions(+), 91 deletions(-) diff --git a/src/plugins/projectexplorer/workspaceproject.cpp b/src/plugins/projectexplorer/workspaceproject.cpp index 7510c3246a8..5dcd8a91983 100644 --- a/src/plugins/projectexplorer/workspaceproject.cpp +++ b/src/plugins/projectexplorer/workspaceproject.cpp @@ -18,8 +18,11 @@ #include "treescanner.h" #include +#include +#include #include +#include #include #include @@ -32,6 +35,8 @@ using namespace Core; namespace ProjectExplorer { +Q_LOGGING_CATEGORY(wsbs, "qtc.projectexplorer.workspacebuildsystem", QtWarningMsg); + const QLatin1StringView FOLDER_MIMETYPE{"inode/directory"}; const QLatin1StringView WORKSPACE_MIMETYPE{"text/x-workspace-project"}; const char WORKSPACE_PROJECT_ID[] = "ProjectExplorer.WorkspaceProject"; @@ -49,113 +54,264 @@ const expected_str projectDefinition(const Project *project) return {}; } +static QFlags workspaceDirFilter = QDir::AllEntries | QDir::NoDotAndDotDot | QDir::Hidden; + class WorkspaceBuildSystem final : 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, [](FileNode *fn) { - return std::unique_ptr(fn); - }); - root->addNestedNodes(std::move(nodePtrs)); - setRootProjectNode(std::move(root)); + WorkspaceBuildSystem(Target *t); - m_parseGuard.markAsSuccess(); - m_parseGuard = {}; + void triggerParsing() final; - emitBuildSystemUpdated(); - }); - 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(); - }); - }); + void watchFolder(const FilePath &path, const QList &versionControls); - connect(target()->project(), - &Project::projectFileIsDirty, - this, - &BuildSystem::requestDelayedParse); + void handleDirectoryChanged(const FilePath &directory); - 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 (const QJsonValue &excludeJson : excludesJson) { - if (excludeJson.isString()) { - FilePath absolute = projectPath.pathAppended(excludeJson.toString()); - m_filters << QRegularExpression( - Utils::wildcardToRegularExpression(absolute.path()), - QRegularExpression::CaseInsensitiveOption); - } - } - - QList 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(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()); - } + void scan(const FilePath &path); + void scanNext(); QString name() const final { return QLatin1String("Workspace"); } private: + bool isFiltered(const FilePath &path, QList versionControls) const; + QList m_filters; + std::unique_ptr m_watcher; ParseGuard m_parseGuard; + FilePaths m_scanQueue; 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> nodePtrs + = Utils::transform(m_scanner.release().allFiles, [](FileNode *fn) { + return std::unique_ptr(fn); + }); + if (scannedDir == projectDirectory()) { + qCDebug(wsbs) << "Finished scanning new root" << scannedDir << "found" + << nodePtrs.size() << "file entries"; + auto root = std::make_unique(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(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 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(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 &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 &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(entry, Node::fileTypeForFileName(entry))); + } + } + QList 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 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 { public: