ACAV f0ba4b7c9529
Abstract Syntax Tree (AST) visualization tool for C, C++, and Objective-C
Loading...
Searching...
No Matches
AstModel.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
23#include "ui/AstModel.h"
24#include "common/FileManager.h"
26#include <QBrush>
27#include <QColor>
28#include <QDebug>
29#include <QFont>
30#include <QStringList>
31
32namespace {
33
34QString valueToString(const acav::AcavJson &value) {
35 if (value.is_boolean()) {
36 return value.get<bool>() ? "true" : "false";
37 }
38 if (value.is_number_integer()) {
39 return QString::number(value.get<int64_t>());
40 }
41 if (value.is_number_unsigned()) {
42 return QString::number(value.get<uint64_t>());
43 }
44 if (value.is_number_float()) {
45 return QString::number(value.get<double>());
46 }
47 if (value.is_string()) {
48 return QString::fromStdString(
49 value.get<acav::InternedString>().str());
50 }
51 return {};
52}
53
54QString buildDetailString(const acav::AcavJson &props) {
55 QStringList attrs;
56 auto addAttr = [&](const char *key, const char *label) {
57 if (props.contains(key) && props.at(key).is_boolean() &&
58 props.at(key).get<bool>()) {
59 attrs << label;
60 }
61 };
62 addAttr("isConstexpr", "constexpr");
63 addAttr("isStatic", "static");
64 addAttr("isVirtual", "virtual");
65 addAttr("isPure", "pure");
66 addAttr("isInlined", "inline");
67
68 QString attrStr;
69 if (!attrs.isEmpty()) {
70 attrStr = " {" + attrs.join(" ") + "}";
71 }
72
73 // Template args
74 QString templateStr;
75 if (props.contains("templateArgs") && props.at("templateArgs").is_array()) {
76 const auto &arr = props.at("templateArgs");
77 if (!arr.empty()) {
78 QStringList parts;
79 for (const auto &arg : arr) {
80 if (arg.contains("value")) {
81 parts << valueToString(arg.at("value"));
82 } else if (arg.contains("kindName")) {
83 parts << QString::fromStdString(
84 arg.at("kindName").get<acav::InternedString>().str());
85 }
86 }
87 templateStr = "<" + parts.join(", ") + ">";
88 }
89 }
90
91 // Value category (Expr only)
92 QString vc;
93 if (props.contains("isLValue") && props.at("isLValue").is_boolean() &&
94 props.at("isLValue").get<bool>()) {
95 vc = " vc=lvalue";
96 } else if (props.contains("isXValue") && props.at("isXValue").is_boolean() &&
97 props.at("isXValue").get<bool>()) {
98 vc = " vc=xvalue";
99 } else if (props.contains("isPRValue") &&
100 props.at("isPRValue").is_boolean() &&
101 props.at("isPRValue").get<bool>()) {
102 vc = " vc=prvalue";
103 }
104
105 // Dependence flag
106 QString dep;
107 const bool typeDep = props.contains("isTypeDependent") &&
108 props.at("isTypeDependent").is_boolean() &&
109 props.at("isTypeDependent").get<bool>();
110 const bool valDep = props.contains("isValueDependent") &&
111 props.at("isValueDependent").is_boolean() &&
112 props.at("isValueDependent").get<bool>();
113 if (typeDep || valDep) {
114 dep = " dep";
115 }
116
117 // Type selections
118 auto getStr = [&](const char *key) -> QString {
119 if (props.contains(key) && props.at(key).is_string()) {
120 return QString::fromStdString(
121 props.at(key).get<acav::InternedString>().str());
122 }
123 return {};
124 };
125 QString type = getStr("type");
126 if (type.isEmpty()) {
127 type = getStr("spelledType");
128 }
129 QString canonical = getStr("canonicalType");
130 QString desugared = getStr("desugaredType");
131
132 // Drop canonical/desugared if identical to primary type
133 if (!canonical.isEmpty() && canonical == type) {
134 canonical.clear();
135 }
136 if (!desugared.isEmpty() && desugared == type) {
137 desugared.clear();
138 }
139
140 QString canonicalPart;
141 if (!canonical.isEmpty()) {
142 canonicalPart = QString(" canon: %1").arg(canonical);
143 }
144 QString desugPart;
145 if (!desugared.isEmpty()) {
146 desugPart = QString(" desug: %1").arg(desugared);
147 }
148
149 QString templatePart;
150 if (!templateStr.isEmpty()) {
151 templatePart = " " + templateStr;
152 }
153
154 QString valuePart;
155 if (props.contains("value")) {
156 QString v = valueToString(props.at("value"));
157 if (!v.isEmpty()) {
158 valuePart = QString(" = %1").arg(v);
159 }
160 }
161
162 QString combined = canonicalPart + desugPart + attrStr + templatePart + vc +
163 dep + valuePart;
164 return combined.trimmed();
165}
166
167} // namespace
168
169namespace acav {
170
171AstModel::AstModel(QObject *parent)
172 : QAbstractItemModel(parent), root_(nullptr),
173 emptyMessage_(tr("No AST available (code not yet compiled).")) {}
174
175AstModel::~AstModel() {
176 // Don't delete nodes - AstContext owns them
177 root_ = nullptr;
178}
179
180void AstModel::setEmptyMessage(const QString &message) {
181 if (emptyMessage_ == message) {
182 return;
183 }
184 beginResetModel();
185 emptyMessage_ = message;
186 endResetModel();
187}
188
190 beginResetModel();
191 root_ = root;
192 selectedNode_ = nullptr;
193 endResetModel();
194}
195
197 beginResetModel();
198 root_ = nullptr;
199 selectedNode_ = nullptr;
200 totalNodeCount_ = 0;
201 endResetModel();
202}
203
205 return static_cast<int>(totalNodeCount_);
206}
207
208QModelIndex AstModel::index(int row, int column,
209 const QModelIndex &parent) const {
210 if (!hasIndex(row, column, parent)) {
211 return QModelIndex();
212 }
213
214 if (!parent.isValid()) {
215 if (row != 0) {
216 return QModelIndex();
217 }
218 if (!root_) {
219 if (emptyMessage_.isEmpty()) {
220 return QModelIndex();
221 }
222 return createIndex(row, column, static_cast<void *>(nullptr));
223 }
224 return createIndex(row, column, root_);
225 }
226
227 AstViewNode *parentNode = getNodeFromIndex(parent);
228 if (!parentNode) {
229 return QModelIndex();
230 }
231 const std::vector<AstViewNode *> &children = visibleChildrenFor(parentNode);
232 if (row < 0 || row >= static_cast<int>(children.size())) {
233 return QModelIndex();
234 }
235
236 AstViewNode *childNode = children[row];
237 return createIndex(row, column, childNode);
238}
239
240QModelIndex AstModel::parent(const QModelIndex &child) const {
241 if (!child.isValid()) {
242 return QModelIndex();
243 }
244
245 AstViewNode *childNode = getNodeFromIndex(child);
246 if (!childNode) {
247 return QModelIndex();
248 }
249
250 AstViewNode *parentNode = childNode->getParent();
251 if (!parentNode) {
252 return QModelIndex();
253 }
254
255 int row = visibleRow(parentNode);
256 if (row < 0) {
257 return QModelIndex();
258 }
259
260 return createIndex(row, 0, parentNode);
261}
262
263int AstModel::rowCount(const QModelIndex &parent) const {
264 if (!root_) {
265 if (!parent.isValid() && !emptyMessage_.isEmpty()) {
266 return 1;
267 }
268 return 0;
269 }
270
271 if (parent.column() > 0) {
272 return 0;
273 }
274
275 if (!parent.isValid()) {
276 return 1;
277 }
278
279 AstViewNode *node = getNodeFromIndex(parent);
280 return static_cast<int>(visibleChildrenFor(node).size());
281}
282
283int AstModel::columnCount(const QModelIndex &parent) const {
284 Q_UNUSED(parent);
285 return 1; // Single column, formatted
286}
287
288QVariant AstModel::data(const QModelIndex &index, int role) const {
289 if (!index.isValid()) {
290 return QVariant();
291 }
292
293 AstViewNode *node = getNodeFromIndex(index);
294 if (!node) {
295 if (!root_ && index.row() == 0 && index.column() == 0) {
296 if (role == Qt::DisplayRole) {
297 return emptyMessage_;
298 }
299 if (role == Qt::ForegroundRole) {
300 return QBrush(QColor(128, 128, 128));
301 }
302 if (role == Qt::FontRole) {
303 QFont font;
304 font.setItalic(true);
305 return font;
306 }
307 if (role == Qt::ToolTipRole) {
308 return tr("Double-click a source file to generate and load an AST.");
309 }
310 }
311 return QVariant();
312 }
313
314 const AcavJson &props = node->getProperties();
315
316 if (role == Qt::DisplayRole && index.column() == 0) {
317 QString kind = "<unknown>";
318 if (props.contains("kind") && props.at("kind").is_string()) {
319 kind = QString::fromStdString(
320 props.at("kind").get<InternedString>().str());
321 }
322
323 QString name;
324 if (props.contains("name") && props.at("name").is_string()) {
325 name = QString::fromStdString(
326 props.at("name").get<InternedString>().str());
327 } else if (props.contains("declName") && props.at("declName").is_string()) {
328 name = QString::fromStdString(
329 props.at("declName").get<InternedString>().str());
330 } else if (props.contains("memberName") &&
331 props.at("memberName").is_string()) {
332 name = QString::fromStdString(
333 props.at("memberName").get<InternedString>().str());
334 }
335
336 QString type;
337 if (props.contains("type") && props.at("type").is_string()) {
338 type = QString::fromStdString(
339 props.at("type").get<InternedString>().str());
340 } else if (props.contains("spelledType") &&
341 props.at("spelledType").is_string()) {
342 type = QString::fromStdString(
343 props.at("spelledType").get<InternedString>().str());
344 }
345
346 QString head = name.isEmpty() ? kind : QString("%1 %2").arg(kind, name);
347 QString tail;
348 if (!type.isEmpty()) {
349 tail = QString(" : %1").arg(type);
350 }
351
352 QString detail = buildDetailString(props);
353 if (detail.isEmpty()) {
354 return head + tail;
355 }
356 return tail.isEmpty() ? (head + " " + detail)
357 : (head + tail + " " + detail);
358 }
359
360 if (role == NodePtrRole) {
361 return QVariant::fromValue(static_cast<void *>(node));
362 }
363
364 if (role == Qt::ToolTipRole) {
365 QString detail = buildDetailString(props);
366 const SourceRange &range = node->getSourceRange();
367 QString loc;
368 if (range.begin().fileID() == FileManager::InvalidFileID) {
369 loc = "Range: <invalid>";
370 } else {
371 loc = QString("Range: %1:%2 - %3:%4")
372 .arg(range.begin().line())
373 .arg(range.begin().column())
374 .arg(range.end().line())
375 .arg(range.end().column());
376 }
377 return detail.isEmpty() ? loc : (detail + "\n" + loc);
378 }
379
380 return QVariant();
381}
382
383QVariant AstModel::headerData(int section, Qt::Orientation orientation,
384 int role) const {
385 if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
386 if (section == 0) {
387 return "AST";
388 }
389 }
390 return QVariant();
391}
392
393Qt::ItemFlags AstModel::flags(const QModelIndex &index) const {
394 if (!index.isValid()) {
395 return Qt::NoItemFlags;
396 }
397
398 if (!root_ && index.row() == 0 && index.column() == 0) {
399 return Qt::ItemIsEnabled;
400 }
401
402 Qt::ItemFlags defaultFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
403
404 // Mark leaf nodes with ItemNeverHasChildren for performance optimization
405 // This avoids unnecessary expand/collapse checks for nodes without children
406 auto *node = static_cast<AstViewNode *>(index.internalPointer());
407 if (node && visibleChildrenFor(node).empty()) {
408 defaultFlags |= Qt::ItemNeverHasChildren;
409 }
410
411 return defaultFlags;
412}
413
414AstViewNode *AstModel::getNodeFromIndex(const QModelIndex &index) const {
415 return static_cast<AstViewNode *>(index.internalPointer());
416}
417
419 if (!node || !root_) {
420 qDebug() << "AstModel::selectNode: null node or no root";
421 return QModelIndex();
422 }
423
424 QModelIndex index = findNodeIndex(node);
425 if (index.isValid()) {
426 // Store old selection to update its display
427 AstViewNode *oldSelection = selectedNode_;
428 QModelIndex oldIndex;
429 if (oldSelection && oldSelection != node) {
430 oldIndex = findNodeIndex(oldSelection);
431 }
432
433 selectedNode_ = node;
434 emit nodeSelected(node);
435
436 // Notify view to repaint old and new selections
437 if (oldIndex.isValid()) {
438 emit dataChanged(oldIndex, oldIndex, {Qt::BackgroundRole});
439 }
440 emit dataChanged(index, index, {Qt::BackgroundRole});
441 } else {
442 qDebug() << "AstModel::selectNode: findNodeIndex failed for node" << node;
443 }
444
445 return index;
446}
447
448void AstModel::updateSelectionFromIndex(const QModelIndex &index) {
449 if (!index.isValid()) {
450 return;
451 }
452
453 AstViewNode *node = getNodeFromIndex(index);
454 if (!node || node == selectedNode_) {
455 return;
456 }
457
458 // Clear old selection
459 QModelIndex oldIndex;
460 if (selectedNode_) {
461 oldIndex = findNodeIndex(selectedNode_);
462 }
463
464 selectedNode_ = node;
465
466 // Notify view to repaint
467 if (oldIndex.isValid()) {
468 emit dataChanged(oldIndex, oldIndex, {Qt::BackgroundRole});
469 }
470 emit dataChanged(index, index, {Qt::BackgroundRole});
471}
472
477QModelIndex AstModel::findNodeIndex(AstViewNode *node) const {
478 if (!node || !root_) {
479 return QModelIndex();
480 }
481
482 std::vector<AstViewNode *> path;
483 AstViewNode *current = node;
484 while (current) {
485 path.push_back(current);
486 current = current->getParent();
487 }
488
489 if (path.back() != root_) {
490 qDebug() << "AstModel::findNodeIndex: path does not lead to root";
491 return QModelIndex();
492 }
493
494 std::reverse(path.begin(), path.end());
495
496 QModelIndex index; // Invalid to start
497 for (AstViewNode *entry : path) {
498 int row = visibleRow(entry);
499 if (row < 0) {
500 qDebug() << "AstModel::findNodeIndex: failed to find row for" << entry;
501 return QModelIndex();
502 }
503 index = this->index(row, 0, index);
504 if (!index.isValid()) {
505 qDebug() << "AstModel::findNodeIndex: failed to create index at row" << row;
506 return QModelIndex();
507 }
508 }
509
510 return index;
511}
512
513const std::vector<AstViewNode *> &
514AstModel::visibleChildrenFor(AstViewNode *parent) const {
515 static const std::vector<AstViewNode *> kEmpty;
516 if (!parent) {
517 return kEmpty;
518 }
519 return parent->getChildren();
520}
521
522int AstModel::visibleRow(AstViewNode *node) const {
523 if (!node) {
524 return -1;
525 }
526
527 AstViewNode *parent = node->getParent();
528 if (!parent) {
529 return node == root_ ? 0 : -1;
530 }
531 const auto &siblings = parent->getChildren();
532 for (std::size_t i = 0; i < siblings.size(); ++i) {
533 if (siblings[i] == node) {
534 return static_cast<int>(i);
535 }
536 }
537 return -1;
538}
539
540} // namespace acav
Qt model for AST tree view display.
nlohmann::basic_json< std::map, std::vector, InternedString, bool, int64_t, uint64_t, double, std::allocator, nlohmann::adl_serializer, std::vector< uint8_t > > AcavJson
Custom JSON type using InternedString for automatic string deduplication.
Definition AstNode.h:51
Centralized file registry with path-to-FileID mapping.
Memory-efficient immutable string with automatic deduplication.
void updateSelectionFromIndex(const QModelIndex &index)
Update selection from a QModelIndex (e.g., when user clicks directly).
Definition AstModel.cpp:448
int visibleNodeCount() const
Number of nodes currently visible.
Definition AstModel.cpp:204
void clear()
Clear model and delete AST.
Definition AstModel.cpp:196
void setRootNode(AstViewNode *root)
Set the root node of AST.
Definition AstModel.cpp:189
QModelIndex selectNode(AstViewNode *node)
Programmatically select node and get its model index.
Definition AstModel.cpp:418
void nodeSelected(AstViewNode *node)
Emitted when node is selected.
void setEmptyMessage(const QString &message)
Message shown when the model has no AST loaded.
Definition AstModel.cpp:180
Represents node in AST tree hierarchy.
Definition AstNode.h:195
Immutable string with automatic deduplication via global pool.
const std::string & str() const
Get the underlying string value.