ACAV f0ba4b7c9529
Abstract Syntax Tree (AST) visualization tool for C, C++, and Objective-C
Loading...
Searching...
No Matches
TranslationUnitModel.cpp
1/*$!{
2* Aurora Clang AST Viewer (ACAV)
3*
4* Copyright (c) 2026 Min Liu
5* Copyright (c) 2026 Michael David Adams
6*
7* SPDX-License-Identifier: GPL-2.0-or-later
8*
9* This program is free software; you can redistribute it and/or modify
10* it under the terms of the GNU General Public License as published by
11* the Free Software Foundation; either version 2 of the License, or
12* (at your option) any later version.
13*
14* This program is distributed in the hope that it will be useful,
15* but WITHOUT ANY WARRANTY; without even the implied warranty of
16* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17* GNU General Public License for more details.
18*
19* You should have received a copy of the GNU General Public License along
20* with this program; if not, see <https://www.gnu.org/licenses/>.
21}$!*/
22
24#include <QCoreApplication>
25#include <QDir>
26#include <QElapsedTimer>
27#include <QEventLoop>
28#include <QFileInfo>
29#include <QJsonArray>
30#include <QSet>
31#include <algorithm>
32#include <functional>
33#include <limits>
34
35namespace acav {
36
37// Custom Qt roles for storing file information
38static constexpr int FilePathRole = Qt::UserRole + 1; // Full file path
39static constexpr int FileIDRole = Qt::UserRole + 2; // FileID (for identification)
40static constexpr int IsSourceFileRole = Qt::UserRole + 3; // True if this is a source file node
41static constexpr int SourceHeadersRole = Qt::UserRole + 4; // QJsonArray of headers for a source file
42static constexpr int HeadersFetchedRole = Qt::UserRole + 5; // Whether headers are populated in tree
43static constexpr int CachedHeaderPathsRole = Qt::UserRole + 6; // QStringList cached header paths
44
45namespace {
46
48QString normalizePath(const QString &path) {
49 if (path.isEmpty()) {
50 return QString();
51 }
52 // Use canonicalFilePath to resolve symlinks (like FileManager does)
53 QFileInfo info(path);
54 QString canonical = info.canonicalFilePath();
55 // canonicalFilePath returns empty if file doesn't exist, fall back to cleanPath
56 if (canonical.isEmpty()) {
57 return QDir::cleanPath(QDir::fromNativeSeparators(path));
58 }
59 return canonical;
60}
61
63QString normalizePathLexical(const QString &path) {
64 if (path.isEmpty()) {
65 return QString();
66 }
67 return QDir::cleanPath(QDir::fromNativeSeparators(path));
68}
69
70bool isPathUnderRoot(const QString &path, const QString &root) {
71 if (root.isEmpty()) {
72 return false;
73 }
74 if (path == root) {
75 return true;
76 }
77 return path.startsWith(root + "/");
78}
79
81struct HeaderClassification {
82 QStringList projectDirect;
83 QStringList projectIndirect;
84 QStringList externalDirect;
85 QStringList externalIndirect;
86};
87
89HeaderClassification classifyHeaders(const QJsonArray &headersArray,
90 const QString &projectRoot) {
91 HeaderClassification result;
92
93 // If project root is empty, we can't meaningfully distinguish
94 // project vs external, so treat all as external
95 bool hasValidProjectRoot = !projectRoot.isEmpty();
96
97 for (const QJsonValue &headerValue : headersArray) {
98 QJsonObject headerObj = headerValue.toObject();
99 QString headerPath = normalizePath(headerObj["path"].toString());
100 bool isDirect = headerObj["direct"].toBool();
101
102 // Classify based on path (inside project root or not)
103 bool isProjectHeader =
104 hasValidProjectRoot && isPathUnderRoot(headerPath, projectRoot);
105
106 if (isProjectHeader) {
107 if (isDirect) {
108 result.projectDirect.append(headerPath);
109 } else {
110 result.projectIndirect.append(headerPath);
111 }
112 } else {
113 if (isDirect) {
114 result.externalDirect.append(headerPath);
115 } else {
116 result.externalIndirect.append(headerPath);
117 }
118 }
119 }
120
121 return result;
122}
123
125struct PathTreeNode {
126 QString name;
127 QString fullPath;
128 QMap<QString, PathTreeNode *> children;
129 bool isFile = false;
130
131 ~PathTreeNode() { qDeleteAll(children); }
132};
133
135void insertPath(PathTreeNode *root, const QString &normalizedPath,
136 const QString &normalizedRoot, const QString &originalFullPath) {
137 // Get relative path from root
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);
147 }
148 }
149 }
150
151 // Split into components
152 QStringList components = relativePath.split('/', Qt::SkipEmptyParts);
153 if (components.isEmpty()) {
154 return;
155 }
156
157 // Navigate/create tree
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);
162
163 if (!current->children.contains(component)) {
164 PathTreeNode *newNode = new PathTreeNode();
165 newNode->name = component;
166 newNode->isFile = isLast;
167 if (isLast) {
168 newNode->fullPath = originalFullPath;
169 }
170 current->children[component] = newNode;
171 }
172 current = current->children[component];
173 }
174}
175
177QList<PathTreeNode *> getSortedChildren(PathTreeNode *node) {
178 QList<PathTreeNode *> dirs, files;
179 for (auto *child : node->children) {
180 if (child->isFile) {
181 files.append(child);
182 } else {
183 dirs.append(child);
184 }
185 }
186
187 auto sortByName = [](PathTreeNode *a, PathTreeNode *b) {
188 return a->name.compare(b->name, Qt::CaseInsensitive) < 0;
189 };
190 std::sort(dirs.begin(), dirs.end(), sortByName);
191 std::sort(files.begin(), files.end(), sortByName);
192
193 return dirs + files;
194}
195
196} // namespace
197
198TranslationUnitModel::TranslationUnitModel(FileManager &fileManager,
199 QObject *parent)
200 : QStandardItemModel(parent), fileManager_(fileManager) {
201 setHorizontalHeaderLabels(QStringList() << "Files");
202}
203
204QString TranslationUnitModel::computeProjectRoot(
205 const QStringList &sourceFilePaths) const {
206 if (sourceFilePaths.isEmpty()) {
207 return QString();
208 }
209
210 if (sourceFilePaths.size() == 1) {
211 // Single file: use its parent directory (normalized)
212 QFileInfo info(sourceFilePaths.first());
213 QString root = normalizePath(info.path());
214 return (root == ".") ? QString() : root;
215 }
216
217 // Split all paths into components (paths are already normalized)
218 QVector<QStringList> allComponents;
219 QString prefix;
220 bool firstPathSet = false;
221 bool firstIsAbsolute = false;
222 for (const QString &path : sourceFilePaths) {
223 bool isAbsolute = QDir::isAbsolutePath(path);
224 if (!firstPathSet) {
225 firstPathSet = true;
226 firstIsAbsolute = isAbsolute;
227 if (path.startsWith("//")) {
228 prefix = "//";
229 } else if (path.startsWith("/")) {
230 prefix = "/";
231 }
232 } else if (isAbsolute != firstIsAbsolute) {
233 return QString();
234 }
235 QStringList components = path.split('/', Qt::SkipEmptyParts);
236 allComponents.append(components);
237 }
238
239 // Find common prefix length
240 int minLength = std::numeric_limits<int>::max();
241 for (const auto &components : allComponents) {
242 minLength = qMin(minLength, components.size());
243 }
244
245 // Find common components (excluding filename, hence -1)
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) {
252 allMatch = false;
253 break;
254 }
255 }
256 if (allMatch) {
257 commonComponents.append(component);
258 } else {
259 break;
260 }
261 }
262
263 if (commonComponents.isEmpty()) {
264 // No common prefix - return empty to indicate all headers are external
265 return QString();
266 }
267
268 QString root = prefix + commonComponents.join('/');
269 return (root == ".") ? QString() : root;
270}
271
272QString TranslationUnitModel::computeProjectRootSmart(
273 const QStringList &sourceFilePaths,
274 const QString &compilationDbPath) const {
275 if (sourceFilePaths.isEmpty()) {
276 return QString();
277 }
278
279 // Step 1: Try simple common ancestor of ALL files
280 QString commonRoot = computeProjectRoot(sourceFilePaths);
281 if (!commonRoot.isEmpty()) {
282 // All files share a common root - use it
283 return commonRoot;
284 }
285
286 // Step 2: Files diverge - group by top-level directory and find majority
287 // e.g., /home/user/project/... vs /tmp/gen/... → group by /home vs /tmp
288 QMap<QString, QStringList> clusters; // top-level dir -> files under it
289
290 for (const QString &path : sourceFilePaths) {
291 QStringList components = path.split('/', Qt::SkipEmptyParts);
292 if (components.isEmpty()) {
293 continue;
294 }
295 // Use first component as cluster key (e.g., "home", "tmp", "var")
296 QString topDir = "/" + components.first();
297 clusters[topDir].append(path);
298 }
299
300 // Step 3: Find cluster with most files
301 QString bestRoot;
302 int maxCount = 0;
303 QStringList tiedRoots;
304
305 for (auto it = clusters.begin(); it != clusters.end(); ++it) {
306 // Compute common root for this cluster
307 QString clusterRoot = computeProjectRoot(it.value());
308 if (clusterRoot.isEmpty()) {
309 clusterRoot = it.key(); // Fall back to top-level dir
310 }
311 int count = it.value().size();
312
313 if (count > maxCount) {
314 maxCount = count;
315 bestRoot = clusterRoot;
316 tiedRoots.clear();
317 tiedRoots.append(clusterRoot);
318 } else if (count == maxCount) {
319 tiedRoots.append(clusterRoot);
320 }
321 }
322
323 // Step 4: If tied, pick the one that is parent of compile_commands.json
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) {
328 return root;
329 }
330 }
331 }
332
333 return bestRoot;
334}
335
336void TranslationUnitModel::buildDirectoryTree(
337 QStandardItem *parent, const QStringList &filePaths,
338 const QString &rootPath, const QHash<QString, QJsonObject> *fileDataMap) {
339
340 if (filePaths.isEmpty()) {
341 return;
342 }
343
344 // Build trie
345 PathTreeNode root;
346 root.name = QFileInfo(rootPath).fileName();
347 if (root.name.isEmpty()) {
348 root.name = rootPath;
349 }
350
351 const QString normalizedRootPath = normalizePathLexical(rootPath);
352
353 for (const QString &path : filePaths) {
354 // filePaths are already normalized by the caller; avoid re-canonicalizing
355 insertPath(&root, path, normalizedRootPath, path);
356 }
357
358 QElapsedTimer yieldTimer;
359 yieldTimer.start();
360
361 auto maybeYield = [&yieldTimer]() {
362 if (yieldTimer.elapsed() >= 35) {
363 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
364 yieldTimer.restart();
365 }
366 };
367
368 // Convert trie to QStandardItem tree (recursive lambda)
369 std::function<void(PathTreeNode *, QStandardItem *)> convertNode =
370 [&](PathTreeNode *node, QStandardItem *parentItem) {
371 QList<PathTreeNode *> sortedChildren = getSortedChildren(node);
372
373 for (PathTreeNode *child : sortedChildren) {
374 maybeYield();
375 QStandardItem *item = new QStandardItem(child->name);
376 item->setEditable(false);
377
378 if (child->isFile) {
379 item->setData(child->fullPath, FilePathRole);
380 item->setToolTip(child->fullPath);
381
382 // Check if this is a source file (has entry in fileDataMap)
383 if (fileDataMap && fileDataMap->contains(child->fullPath)) {
384 // Register with FileManager and store FileID
385 FileID fileId =
386 fileManager_.registerFile(child->fullPath.toStdString());
387 item->setData(QVariant::fromValue(fileId), FileIDRole);
388 item->setData(true, IsSourceFileRole);
389
390 // Get header count for tooltip
391 QJsonObject fileObj = fileDataMap->value(child->fullPath);
392 int headerCount = fileObj["headerCount"].toInt();
393 item->setToolTip(
394 QString("%1\n%2 headers").arg(child->fullPath).arg(headerCount));
395
396 // Store headers for lazy population when expanded / needed.
397 item->setData(fileObj["headers"].toArray(), SourceHeadersRole);
398 item->setData(false, HeadersFetchedRole);
399 sourceItemByPath_.insert(child->fullPath, item);
400 }
401 } else {
402 // Directory node
403 item->setData(QString(), FilePathRole);
404 convertNode(child, item);
405 }
406
407 parentItem->appendRow(item);
408 }
409 };
410
411 convertNode(&root, parent);
412}
413
414void TranslationUnitModel::addHeaderCategory(QStandardItem *parent,
415 const QString &categoryName,
416 const QStringList &headers,
417 const QString &rootPath) {
418 if (headers.isEmpty()) {
419 return;
420 }
421
422 QStandardItem *categoryItem = new QStandardItem(categoryName);
423 categoryItem->setEditable(false);
424 categoryItem->setData(QString(), FilePathRole);
425
426 // Build tree for these headers
427 buildDirectoryTree(categoryItem, headers, rootPath, nullptr);
428
429 if (categoryItem->rowCount() > 0) {
430 parent->appendRow(categoryItem);
431 } else {
432 delete categoryItem;
433 }
434}
435
437 const QJsonObject &dependenciesJson, const QString &overrideProjectRoot,
438 const QString &compilationDbPath) {
439 clear();
440
441 QJsonArray filesArray = dependenciesJson["files"].toArray();
442
443 // Phase 1: Collect all unique source file paths and their data
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());
450
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);
458 }
459 }
460
461 if (sourceFilePaths.isEmpty()) {
462 return;
463 }
464
465 // Phase 2: Determine project root
466 // Priority: 1) user override, 2) compute from source files
467 if (!overrideProjectRoot.isEmpty()) {
468 projectRoot_ = normalizePath(overrideProjectRoot);
469 } else {
470 projectRoot_ = computeProjectRootSmart(sourceFilePaths, compilationDbPath);
471 }
472
473 // Phase 3: Classify source files into project and external
474 QStringList projectFiles;
475 QStringList externalFiles;
476
477 for (const QString &path : sourceFilePaths) {
478 if (isPathUnderRoot(path, projectRoot_)) {
479 projectFiles.append(path);
480 } else {
481 externalFiles.append(path);
482 }
483 }
484
485 // Phase 4: Create "Project Files" root node
486 if (!projectFiles.isEmpty()) {
487 QString projectDisplayName =
488 projectRoot_.isEmpty()
489 ? QStringLiteral("Project Files")
490 : QStringLiteral("Project Files (%1)")
491 .arg(QFileInfo(projectRoot_).fileName());
492
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_);
498
499 buildDirectoryTree(projectRootItem, projectFiles, projectRoot_,
500 &fileDataMap);
501 invisibleRootItem()->appendRow(projectRootItem);
502 }
503
504 // Phase 5: Create "External Files" root node (relative to /)
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 /)"));
511
512 // Use "/" as root for external files so paths are shown as absolute-style
513 buildDirectoryTree(externalRootItem, externalFiles, QStringLiteral("/"),
514 &fileDataMap);
515 invisibleRootItem()->appendRow(externalRootItem);
516 }
517}
518
519bool TranslationUnitModel::hasChildren(const QModelIndex &parent) const {
520 if (parent.isValid() && canFetchMore(parent)) {
521 return true;
522 }
523 return QStandardItemModel::hasChildren(parent);
524}
525
526bool TranslationUnitModel::canFetchMore(const QModelIndex &parent) const {
527 if (!parent.isValid()) {
528 return false;
529 }
530 QStandardItem *item = itemFromIndex(parent);
531 if (!item) {
532 return false;
533 }
534 if (!item->data(IsSourceFileRole).toBool()) {
535 return false;
536 }
537 if (item->data(HeadersFetchedRole).toBool()) {
538 return false;
539 }
540 QJsonArray headersArray = item->data(SourceHeadersRole).toJsonArray();
541 return !headersArray.isEmpty();
542}
543
544void TranslationUnitModel::fetchMore(const QModelIndex &parent) {
545 if (!canFetchMore(parent)) {
546 return;
547 }
548 QStandardItem *item = itemFromIndex(parent);
549 if (!item) {
550 return;
551 }
552 populateHeadersForSourceItem(item);
553}
554
555void TranslationUnitModel::populateHeadersForSourceItem(
556 QStandardItem *sourceItem) {
557 if (!sourceItem) {
558 return;
559 }
560 if (!sourceItem->data(IsSourceFileRole).toBool()) {
561 return;
562 }
563 if (sourceItem->data(HeadersFetchedRole).toBool()) {
564 return;
565 }
566
567 QJsonArray headersArray = sourceItem->data(SourceHeadersRole).toJsonArray();
568 if (headersArray.isEmpty()) {
569 sourceItem->setData(true, HeadersFetchedRole);
570 return;
571 }
572
573 HeaderClassification headers = classifyHeaders(headersArray, projectRoot_);
574
575 // Project Headers
576 if (!headers.projectDirect.isEmpty() || !headers.projectIndirect.isEmpty()) {
577 QStandardItem *projectHeaders = new QStandardItem("Project Headers");
578 projectHeaders->setEditable(false);
579 projectHeaders->setData(QString(), FilePathRole);
580
581 addHeaderCategory(projectHeaders, "Direct", headers.projectDirect,
582 projectRoot_);
583 addHeaderCategory(projectHeaders, "Indirect", headers.projectIndirect,
584 projectRoot_);
585
586 if (projectHeaders->rowCount() > 0) {
587 sourceItem->appendRow(projectHeaders);
588 } else {
589 delete projectHeaders;
590 }
591 }
592
593 // External Headers
594 if (!headers.externalDirect.isEmpty() || !headers.externalIndirect.isEmpty()) {
595 QStandardItem *externalHeaders = new QStandardItem("External Headers");
596 externalHeaders->setEditable(false);
597 externalHeaders->setData(QString(), FilePathRole);
598
599 QStringList allExternal = headers.externalDirect + headers.externalIndirect;
600 QString externalRoot = computeProjectRoot(allExternal);
601
602 addHeaderCategory(externalHeaders, "Direct", headers.externalDirect,
603 externalRoot);
604 addHeaderCategory(externalHeaders, "Indirect", headers.externalIndirect,
605 externalRoot);
606
607 if (externalHeaders->rowCount() > 0) {
608 sourceItem->appendRow(externalHeaders);
609 } else {
610 delete externalHeaders;
611 }
612 }
613
614 sourceItem->setData(true, HeadersFetchedRole);
615}
616
618 const QModelIndex &index) const {
619 if (!index.isValid()) {
620 return QString();
621 }
622
623 QStandardItem *item = itemFromIndex(index);
624 if (!item) {
625 return QString();
626 }
627
628 QVariant pathVariant = item->data(FilePathRole);
629 return pathVariant.toString();
630}
631
633 const QString &sourceFilePath) const {
634 QStringList headers;
635
636 QString normalizedSourcePath = normalizePath(sourceFilePath);
637 QModelIndex sourceIndex = findIndexByFilePath(normalizedSourcePath);
638 if (!sourceIndex.isValid()) {
639 return headers;
640 }
641
642 QStandardItem *sourceItem = itemFromIndex(sourceIndex);
643 if (!sourceItem) {
644 return headers;
645 }
646
647 QVariant cached = sourceItem->data(CachedHeaderPathsRole);
648 if (cached.isValid()) {
649 return cached.toStringList();
650 }
651
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);
660 }
661 }
662 sourceItem->setData(headers, CachedHeaderPathsRole);
663 return headers;
664 }
665
666 // Fallback: if headers have been populated in the tree, collect them.
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);
672 }
673 for (int i = 0; i < item->rowCount(); ++i) {
674 collectHeaders(item->child(i));
675 }
676 };
677
678 collectHeaders(sourceItem);
679 sourceItem->setData(headers, CachedHeaderPathsRole);
680 return headers;
681}
682
683QModelIndex
684TranslationUnitModel::findIndexByFilePath(const QString &filePath) const {
685 QString normalizedPath = normalizePath(filePath);
686 auto it = sourceItemByPath_.find(normalizedPath);
687 if (it != sourceItemByPath_.end() && it.value()) {
688 return indexFromItem(it.value());
689 }
690 // Recursive search through tree
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);
697 }
698
699 for (int i = 0; i < item->rowCount(); ++i) {
700 QModelIndex found = searchItem(item->child(i));
701 if (found.isValid()) {
702 return found;
703 }
704 }
705
706 return QModelIndex();
707 };
708
709 for (int i = 0; i < invisibleRootItem()->rowCount(); ++i) {
710 QModelIndex found = searchItem(invisibleRootItem()->child(i));
711 if (found.isValid()) {
712 return found;
713 }
714 }
715
716 return QModelIndex();
717}
718
719QModelIndex
720TranslationUnitModel::findIndexByAnyFilePath(const QString &filePath) const {
721 QString normalizedPath = normalizePath(filePath);
722
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);
728 }
729
730 for (int i = 0; i < item->rowCount(); ++i) {
731 QModelIndex found = searchItem(item->child(i));
732 if (found.isValid()) {
733 return found;
734 }
735 }
736
737 return QModelIndex();
738 };
739
740 for (int i = 0; i < invisibleRootItem()->rowCount(); ++i) {
741 QModelIndex found = searchItem(invisibleRootItem()->child(i));
742 if (found.isValid()) {
743 return found;
744 }
745 }
746
747 return QModelIndex();
748}
749
750QModelIndex TranslationUnitModel::findIndexByAnyFilePathUnder(
751 const QString &filePath, const QModelIndex &root) const {
752 QString normalizedPath = normalizePath(filePath);
753 if (!root.isValid()) {
754 return QModelIndex();
755 }
756
757 QStandardItem *rootItem = itemFromIndex(root);
758 if (!rootItem) {
759 return QModelIndex();
760 }
761
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);
767 }
768
769 for (int i = 0; i < item->rowCount(); ++i) {
770 QModelIndex found = searchItem(item->child(i));
771 if (found.isValid()) {
772 return found;
773 }
774 }
775
776 return QModelIndex();
777 };
778
779 return searchItem(rootItem);
780}
781
783 // Recursive search through tree
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);
789 }
790
791 for (int i = 0; i < item->rowCount(); ++i) {
792 QModelIndex found = searchItem(item->child(i));
793 if (found.isValid()) {
794 return found;
795 }
796 }
797
798 return QModelIndex();
799 };
800
801 for (int i = 0; i < invisibleRootItem()->rowCount(); ++i) {
802 QModelIndex found = searchItem(invisibleRootItem()->child(i));
803 if (found.isValid()) {
804 return found;
805 }
806 }
807
808 return QModelIndex();
809}
810
812 projectRoot_.clear();
813 sourceItemByPath_.clear();
814 QStandardItemModel::clear();
815 setHorizontalHeaderLabels(QStringList() << "Files");
816}
817
818} // namespace acav
std::size_t FileID
Type-safe identifier for registered files. 0 is reserved for invalid.
Definition FileManager.h:38
Qt model for translation unit tree view.
Centralized file registry providing path-to-FileID mapping.
Definition FileManager.h:45
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.