24#include <QCoreApplication>
26#include <QElapsedTimer>
38static constexpr int FilePathRole = Qt::UserRole + 1;
39static constexpr int FileIDRole = Qt::UserRole + 2;
40static constexpr int IsSourceFileRole = Qt::UserRole + 3;
41static constexpr int SourceHeadersRole = Qt::UserRole + 4;
42static constexpr int HeadersFetchedRole = Qt::UserRole + 5;
43static constexpr int CachedHeaderPathsRole = Qt::UserRole + 6;
48QString normalizePath(
const QString &path) {
54 QString canonical = info.canonicalFilePath();
56 if (canonical.isEmpty()) {
57 return QDir::cleanPath(QDir::fromNativeSeparators(path));
63QString normalizePathLexical(
const QString &path) {
67 return QDir::cleanPath(QDir::fromNativeSeparators(path));
70bool isPathUnderRoot(
const QString &path,
const QString &root) {
77 return path.startsWith(root +
"/");
81struct HeaderClassification {
82 QStringList projectDirect;
83 QStringList projectIndirect;
84 QStringList externalDirect;
85 QStringList externalIndirect;
89HeaderClassification classifyHeaders(
const QJsonArray &headersArray,
90 const QString &projectRoot) {
91 HeaderClassification result;
95 bool hasValidProjectRoot = !projectRoot.isEmpty();
97 for (
const QJsonValue &headerValue : headersArray) {
98 QJsonObject headerObj = headerValue.toObject();
99 QString headerPath = normalizePath(headerObj[
"path"].toString());
100 bool isDirect = headerObj[
"direct"].toBool();
103 bool isProjectHeader =
104 hasValidProjectRoot && isPathUnderRoot(headerPath, projectRoot);
106 if (isProjectHeader) {
108 result.projectDirect.append(headerPath);
110 result.projectIndirect.append(headerPath);
114 result.externalDirect.append(headerPath);
116 result.externalIndirect.append(headerPath);
128 QMap<QString, PathTreeNode *> children;
131 ~PathTreeNode() { qDeleteAll(children); }
135void insertPath(PathTreeNode *root,
const QString &normalizedPath,
136 const QString &normalizedRoot,
const QString &originalFullPath) {
138 QString relativePath = normalizedPath;
139 if (!normalizedRoot.isEmpty()) {
140 if (normalizedPath.startsWith(normalizedRoot +
"/")) {
141 relativePath = normalizedPath.mid(normalizedRoot.length() + 1);
142 }
else if (normalizedPath.startsWith(normalizedRoot) &&
143 normalizedPath.length() > normalizedRoot.length()) {
144 relativePath = normalizedPath.mid(normalizedRoot.length());
145 if (relativePath.startsWith(
'/')) {
146 relativePath = relativePath.mid(1);
152 QStringList components = relativePath.split(
'/', Qt::SkipEmptyParts);
153 if (components.isEmpty()) {
158 PathTreeNode *current = root;
159 for (
int i = 0; i < components.size(); ++i) {
160 const QString &component = components[i];
161 bool isLast = (i == components.size() - 1);
163 if (!current->children.contains(component)) {
164 PathTreeNode *newNode =
new PathTreeNode();
165 newNode->name = component;
166 newNode->isFile = isLast;
168 newNode->fullPath = originalFullPath;
170 current->children[component] = newNode;
172 current = current->children[component];
177QList<PathTreeNode *> getSortedChildren(PathTreeNode *node) {
178 QList<PathTreeNode *> dirs, files;
179 for (
auto *child : node->children) {
187 auto sortByName = [](PathTreeNode *a, PathTreeNode *b) {
188 return a->name.compare(b->name, Qt::CaseInsensitive) < 0;
190 std::sort(dirs.begin(), dirs.end(), sortByName);
191 std::sort(files.begin(), files.end(), sortByName);
198TranslationUnitModel::TranslationUnitModel(
FileManager &fileManager,
200 : QStandardItemModel(parent), fileManager_(fileManager) {
201 setHorizontalHeaderLabels(QStringList() <<
"Files");
204QString TranslationUnitModel::computeProjectRoot(
205 const QStringList &sourceFilePaths)
const {
206 if (sourceFilePaths.isEmpty()) {
210 if (sourceFilePaths.size() == 1) {
212 QFileInfo info(sourceFilePaths.first());
213 QString root = normalizePath(info.path());
214 return (root ==
".") ? QString() : root;
218 QVector<QStringList> allComponents;
220 bool firstPathSet =
false;
221 bool firstIsAbsolute =
false;
222 for (
const QString &path : sourceFilePaths) {
223 bool isAbsolute = QDir::isAbsolutePath(path);
226 firstIsAbsolute = isAbsolute;
227 if (path.startsWith(
"//")) {
229 }
else if (path.startsWith(
"/")) {
232 }
else if (isAbsolute != firstIsAbsolute) {
235 QStringList components = path.split(
'/', Qt::SkipEmptyParts);
236 allComponents.append(components);
240 int minLength = std::numeric_limits<int>::max();
241 for (
const auto &components : allComponents) {
242 minLength = qMin(minLength, components.size());
246 QStringList commonComponents;
247 for (
int i = 0; i < minLength - 1; ++i) {
248 QString component = allComponents[0][i];
249 bool allMatch =
true;
250 for (
int j = 1; j < allComponents.size(); ++j) {
251 if (allComponents[j][i] != component) {
257 commonComponents.append(component);
263 if (commonComponents.isEmpty()) {
268 QString root = prefix + commonComponents.join(
'/');
269 return (root ==
".") ? QString() : root;
272QString TranslationUnitModel::computeProjectRootSmart(
273 const QStringList &sourceFilePaths,
274 const QString &compilationDbPath)
const {
275 if (sourceFilePaths.isEmpty()) {
280 QString commonRoot = computeProjectRoot(sourceFilePaths);
281 if (!commonRoot.isEmpty()) {
288 QMap<QString, QStringList> clusters;
290 for (
const QString &path : sourceFilePaths) {
291 QStringList components = path.split(
'/', Qt::SkipEmptyParts);
292 if (components.isEmpty()) {
296 QString topDir =
"/" + components.first();
297 clusters[topDir].append(path);
303 QStringList tiedRoots;
305 for (
auto it = clusters.begin(); it != clusters.end(); ++it) {
307 QString clusterRoot = computeProjectRoot(it.value());
308 if (clusterRoot.isEmpty()) {
309 clusterRoot = it.key();
311 int count = it.value().size();
313 if (count > maxCount) {
315 bestRoot = clusterRoot;
317 tiedRoots.append(clusterRoot);
318 }
else if (count == maxCount) {
319 tiedRoots.append(clusterRoot);
324 if (tiedRoots.size() > 1 && !compilationDbPath.isEmpty()) {
325 QString dbDir = normalizePath(QFileInfo(compilationDbPath).absolutePath());
326 for (
const QString &root : tiedRoots) {
327 if (dbDir.startsWith(root +
"/") || dbDir == root) {
336void TranslationUnitModel::buildDirectoryTree(
337 QStandardItem *parent,
const QStringList &filePaths,
338 const QString &rootPath,
const QHash<QString, QJsonObject> *fileDataMap) {
340 if (filePaths.isEmpty()) {
346 root.name = QFileInfo(rootPath).fileName();
347 if (root.name.isEmpty()) {
348 root.name = rootPath;
351 const QString normalizedRootPath = normalizePathLexical(rootPath);
353 for (
const QString &path : filePaths) {
355 insertPath(&root, path, normalizedRootPath, path);
358 QElapsedTimer yieldTimer;
361 auto maybeYield = [&yieldTimer]() {
362 if (yieldTimer.elapsed() >= 35) {
363 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
364 yieldTimer.restart();
369 std::function<void(PathTreeNode *, QStandardItem *)> convertNode =
370 [&](PathTreeNode *node, QStandardItem *parentItem) {
371 QList<PathTreeNode *> sortedChildren = getSortedChildren(node);
373 for (PathTreeNode *child : sortedChildren) {
375 QStandardItem *item =
new QStandardItem(child->name);
376 item->setEditable(
false);
379 item->setData(child->fullPath, FilePathRole);
380 item->setToolTip(child->fullPath);
383 if (fileDataMap && fileDataMap->contains(child->fullPath)) {
386 fileManager_.registerFile(child->fullPath.toStdString());
387 item->setData(QVariant::fromValue(fileId), FileIDRole);
388 item->setData(
true, IsSourceFileRole);
391 QJsonObject fileObj = fileDataMap->value(child->fullPath);
392 int headerCount = fileObj[
"headerCount"].toInt();
394 QString(
"%1\n%2 headers").arg(child->fullPath).arg(headerCount));
397 item->setData(fileObj[
"headers"].toArray(), SourceHeadersRole);
398 item->setData(
false, HeadersFetchedRole);
399 sourceItemByPath_.insert(child->fullPath, item);
403 item->setData(QString(), FilePathRole);
404 convertNode(child, item);
407 parentItem->appendRow(item);
411 convertNode(&root, parent);
414void TranslationUnitModel::addHeaderCategory(QStandardItem *parent,
415 const QString &categoryName,
416 const QStringList &headers,
417 const QString &rootPath) {
418 if (headers.isEmpty()) {
422 QStandardItem *categoryItem =
new QStandardItem(categoryName);
423 categoryItem->setEditable(
false);
424 categoryItem->setData(QString(), FilePathRole);
427 buildDirectoryTree(categoryItem, headers, rootPath,
nullptr);
429 if (categoryItem->rowCount() > 0) {
430 parent->appendRow(categoryItem);
437 const QJsonObject &dependenciesJson,
const QString &overrideProjectRoot,
438 const QString &compilationDbPath) {
441 QJsonArray filesArray = dependenciesJson[
"files"].toArray();
444 QSet<QString> seenPaths;
445 QStringList sourceFilePaths;
446 QHash<QString, QJsonObject> fileDataMap;
447 sourceFilePaths.reserve(filesArray.size());
448 seenPaths.reserve(filesArray.size());
449 fileDataMap.reserve(filesArray.size());
451 for (
const QJsonValue &fileValue : filesArray) {
452 QJsonObject fileObj = fileValue.toObject();
453 QString filePath = normalizePath(fileObj[
"path"].toString());
454 if (!seenPaths.contains(filePath)) {
455 seenPaths.insert(filePath);
456 sourceFilePaths.append(filePath);
457 fileDataMap.insert(filePath, fileObj);
461 if (sourceFilePaths.isEmpty()) {
467 if (!overrideProjectRoot.isEmpty()) {
468 projectRoot_ = normalizePath(overrideProjectRoot);
470 projectRoot_ = computeProjectRootSmart(sourceFilePaths, compilationDbPath);
474 QStringList projectFiles;
475 QStringList externalFiles;
477 for (
const QString &path : sourceFilePaths) {
478 if (isPathUnderRoot(path, projectRoot_)) {
479 projectFiles.append(path);
481 externalFiles.append(path);
486 if (!projectFiles.isEmpty()) {
487 QString projectDisplayName =
488 projectRoot_.isEmpty()
489 ? QStringLiteral(
"Project Files")
490 : QStringLiteral(
"Project Files (%1)")
491 .arg(QFileInfo(projectRoot_).fileName());
493 QStandardItem *projectRootItem =
new QStandardItem(projectDisplayName);
494 projectRootItem->setEditable(
false);
495 projectRootItem->setData(QString(), FilePathRole);
496 projectRootItem->setToolTip(
497 projectRoot_.isEmpty() ? tr(
"Project files") : projectRoot_);
499 buildDirectoryTree(projectRootItem, projectFiles, projectRoot_,
501 invisibleRootItem()->appendRow(projectRootItem);
505 if (!externalFiles.isEmpty()) {
506 QStandardItem *externalRootItem =
507 new QStandardItem(QStringLiteral(
"External Files"));
508 externalRootItem->setEditable(
false);
509 externalRootItem->setData(QString(), FilePathRole);
510 externalRootItem->setToolTip(tr(
"Files outside project root (relative to /)"));
513 buildDirectoryTree(externalRootItem, externalFiles, QStringLiteral(
"/"),
515 invisibleRootItem()->appendRow(externalRootItem);
519bool TranslationUnitModel::hasChildren(
const QModelIndex &parent)
const {
520 if (parent.isValid() && canFetchMore(parent)) {
523 return QStandardItemModel::hasChildren(parent);
526bool TranslationUnitModel::canFetchMore(
const QModelIndex &parent)
const {
527 if (!parent.isValid()) {
530 QStandardItem *item = itemFromIndex(parent);
534 if (!item->data(IsSourceFileRole).toBool()) {
537 if (item->data(HeadersFetchedRole).toBool()) {
540 QJsonArray headersArray = item->data(SourceHeadersRole).toJsonArray();
541 return !headersArray.isEmpty();
544void TranslationUnitModel::fetchMore(
const QModelIndex &parent) {
545 if (!canFetchMore(parent)) {
548 QStandardItem *item = itemFromIndex(parent);
552 populateHeadersForSourceItem(item);
555void TranslationUnitModel::populateHeadersForSourceItem(
556 QStandardItem *sourceItem) {
560 if (!sourceItem->data(IsSourceFileRole).toBool()) {
563 if (sourceItem->data(HeadersFetchedRole).toBool()) {
567 QJsonArray headersArray = sourceItem->data(SourceHeadersRole).toJsonArray();
568 if (headersArray.isEmpty()) {
569 sourceItem->setData(
true, HeadersFetchedRole);
573 HeaderClassification headers = classifyHeaders(headersArray, projectRoot_);
576 if (!headers.projectDirect.isEmpty() || !headers.projectIndirect.isEmpty()) {
577 QStandardItem *projectHeaders =
new QStandardItem(
"Project Headers");
578 projectHeaders->setEditable(
false);
579 projectHeaders->setData(QString(), FilePathRole);
581 addHeaderCategory(projectHeaders,
"Direct", headers.projectDirect,
583 addHeaderCategory(projectHeaders,
"Indirect", headers.projectIndirect,
586 if (projectHeaders->rowCount() > 0) {
587 sourceItem->appendRow(projectHeaders);
589 delete projectHeaders;
594 if (!headers.externalDirect.isEmpty() || !headers.externalIndirect.isEmpty()) {
595 QStandardItem *externalHeaders =
new QStandardItem(
"External Headers");
596 externalHeaders->setEditable(
false);
597 externalHeaders->setData(QString(), FilePathRole);
599 QStringList allExternal = headers.externalDirect + headers.externalIndirect;
600 QString externalRoot = computeProjectRoot(allExternal);
602 addHeaderCategory(externalHeaders,
"Direct", headers.externalDirect,
604 addHeaderCategory(externalHeaders,
"Indirect", headers.externalIndirect,
607 if (externalHeaders->rowCount() > 0) {
608 sourceItem->appendRow(externalHeaders);
610 delete externalHeaders;
614 sourceItem->setData(
true, HeadersFetchedRole);
618 const QModelIndex &index)
const {
619 if (!index.isValid()) {
623 QStandardItem *item = itemFromIndex(index);
628 QVariant pathVariant = item->data(FilePathRole);
629 return pathVariant.toString();
633 const QString &sourceFilePath)
const {
636 QString normalizedSourcePath = normalizePath(sourceFilePath);
638 if (!sourceIndex.isValid()) {
642 QStandardItem *sourceItem = itemFromIndex(sourceIndex);
647 QVariant cached = sourceItem->data(CachedHeaderPathsRole);
648 if (cached.isValid()) {
649 return cached.toStringList();
652 QJsonArray headersArray = sourceItem->data(SourceHeadersRole).toJsonArray();
653 if (!headersArray.isEmpty()) {
654 headers.reserve(headersArray.size());
655 for (
const QJsonValue &headerValue : headersArray) {
656 QJsonObject headerObj = headerValue.toObject();
657 QString headerPath = normalizePath(headerObj[
"path"].toString());
658 if (!headerPath.isEmpty()) {
659 headers.append(headerPath);
662 sourceItem->setData(headers, CachedHeaderPathsRole);
667 std::function<void(QStandardItem *)> collectHeaders =
668 [&](QStandardItem *item) {
669 QString path = item->data(FilePathRole).toString();
670 if (!path.isEmpty() && path != normalizedSourcePath) {
671 headers.append(path);
673 for (
int i = 0; i < item->rowCount(); ++i) {
674 collectHeaders(item->child(i));
678 collectHeaders(sourceItem);
679 sourceItem->setData(headers, CachedHeaderPathsRole);
685 QString normalizedPath = normalizePath(filePath);
686 auto it = sourceItemByPath_.find(normalizedPath);
687 if (it != sourceItemByPath_.end() && it.value()) {
688 return indexFromItem(it.value());
691 std::function<QModelIndex(QStandardItem *)> searchItem =
692 [&](QStandardItem *item) -> QModelIndex {
693 QString itemPath = item->data(FilePathRole).toString();
694 if (itemPath == normalizedPath &&
695 item->data(IsSourceFileRole).toBool()) {
696 return indexFromItem(item);
699 for (
int i = 0; i < item->rowCount(); ++i) {
700 QModelIndex found = searchItem(item->child(i));
701 if (found.isValid()) {
706 return QModelIndex();
709 for (
int i = 0; i < invisibleRootItem()->rowCount(); ++i) {
710 QModelIndex found = searchItem(invisibleRootItem()->child(i));
711 if (found.isValid()) {
716 return QModelIndex();
720TranslationUnitModel::findIndexByAnyFilePath(
const QString &filePath)
const {
721 QString normalizedPath = normalizePath(filePath);
723 std::function<QModelIndex(QStandardItem *)> searchItem =
724 [&](QStandardItem *item) -> QModelIndex {
725 QString itemPath = item->data(FilePathRole).toString();
726 if (!itemPath.isEmpty() && itemPath == normalizedPath) {
727 return indexFromItem(item);
730 for (
int i = 0; i < item->rowCount(); ++i) {
731 QModelIndex found = searchItem(item->child(i));
732 if (found.isValid()) {
737 return QModelIndex();
740 for (
int i = 0; i < invisibleRootItem()->rowCount(); ++i) {
741 QModelIndex found = searchItem(invisibleRootItem()->child(i));
742 if (found.isValid()) {
747 return QModelIndex();
750QModelIndex TranslationUnitModel::findIndexByAnyFilePathUnder(
751 const QString &filePath,
const QModelIndex &root)
const {
752 QString normalizedPath = normalizePath(filePath);
753 if (!root.isValid()) {
754 return QModelIndex();
757 QStandardItem *rootItem = itemFromIndex(root);
759 return QModelIndex();
762 std::function<QModelIndex(QStandardItem *)> searchItem =
763 [&](QStandardItem *item) -> QModelIndex {
764 QString itemPath = item->data(FilePathRole).toString();
765 if (!itemPath.isEmpty() && itemPath == normalizedPath) {
766 return indexFromItem(item);
769 for (
int i = 0; i < item->rowCount(); ++i) {
770 QModelIndex found = searchItem(item->child(i));
771 if (found.isValid()) {
776 return QModelIndex();
779 return searchItem(rootItem);
784 std::function<QModelIndex(QStandardItem *)> searchItem =
785 [&](QStandardItem *item) -> QModelIndex {
786 QVariant fileIdVariant = item->data(FileIDRole);
787 if (fileIdVariant.isValid() && fileIdVariant.value<
FileID>() == fileId) {
788 return indexFromItem(item);
791 for (
int i = 0; i < item->rowCount(); ++i) {
792 QModelIndex found = searchItem(item->child(i));
793 if (found.isValid()) {
798 return QModelIndex();
801 for (
int i = 0; i < invisibleRootItem()->rowCount(); ++i) {
802 QModelIndex found = searchItem(invisibleRootItem()->child(i));
803 if (found.isValid()) {
808 return QModelIndex();
812 projectRoot_.clear();
813 sourceItemByPath_.clear();
814 QStandardItemModel::clear();
815 setHorizontalHeaderLabels(QStringList() <<
"Files");
std::size_t FileID
Type-safe identifier for registered files. 0 is reserved for invalid.
Qt model for translation unit tree view.
Centralized file registry providing path-to-FileID mapping.
QString getSourceFilePathFromIndex(const QModelIndex &index) const
Get the source file path from a model index.
QModelIndex findIndexByFilePath(const QString &filePath) const
Find model index by file path (legacy method for compatibility).
void clear()
Clear all data from the model.
QModelIndex findIndexByFileId(FileID fileId) const
Find model index by FileID.
QStringList getIncludedHeadersForSource(const QString &sourceFilePath) const
Get all included headers for a source file.
void populateFromDependencies(const QJsonObject &dependenciesJson, const QString &overrideProjectRoot=QString(), const QString &compilationDbPath=QString())
Populate model from query-dependencies JSON output.