32#include <QAbstractItemView>
34#include <QApplication>
35#include <QButtonGroup>
40#include <QDesktopServices>
41#include <QDialogButtonBox>
48#include <QFontMetrics>
54#include <QKeySequence>
64#include <QPlainTextEdit>
66#include <QProgressDialog>
68#include <QRadioButton>
69#include <QRegularExpression>
72#include <QStringListModel>
76#include <QTableWidget>
79#include <QResizeEvent>
86#include <unordered_set>
91MainWindow::MainWindow(QWidget *parent)
92 : QMainWindow(parent), fileMenu_(nullptr), viewMenu_(nullptr),
93 openAction_(nullptr), exitAction_(nullptr), settingsAction_(nullptr),
94 reloadConfigAction_(nullptr), navBackAction_(nullptr),
95 navForwardAction_(nullptr), goToMacroDefinitionAction_(nullptr),
96 zoomInAction_(nullptr), zoomOutAction_(nullptr),
97 zoomResetAction_(nullptr), navToolBar_(nullptr),
98 resetLayoutAction_(nullptr), tuDock_(nullptr), sourceDock_(nullptr),
99 astDock_(nullptr), declContextDock_(nullptr), logDock_(nullptr),
100 tuView_(nullptr), sourceView_(nullptr), astView_(nullptr),
101 declContextView_(nullptr), tuModel_(nullptr), astModel_(nullptr),
102 queryRunner_(nullptr), parallelQueryRunner_(nullptr),
103 makeAstRunner_(nullptr), astExtractorRunner_(nullptr),
104 astWorkerThread_(nullptr), sourceSearchInput_(nullptr),
105 sourceSearchPrevButton_(nullptr), sourceSearchNextButton_(nullptr),
106 sourceSearchStatus_(nullptr), sourceSearchDebounce_(nullptr),
107 nodeCycleWidget_(nullptr), isAstExtractionInProgress_(false) {
109 qRegisterMetaType<LogEntry>();
113 setWindowTitle(
"ACAV");
114 setWindowIcon(QIcon(
":/resources/aurora_icon.png"));
125 applyFontSize(AppConfig::instance().getFontSize());
127 logStatus(LogLevel::Info, tr(
"Ready"));
130MainWindow::~MainWindow() {
131 QObject::disconnect(qApp, &QApplication::focusChanged,
this,
132 &MainWindow::onFocusChanged);
133 if (makeAstRunner_) {
134 QObject::disconnect(makeAstRunner_, &MakeAstRunner::logMessage,
this,
135 &MainWindow::onMakeAstLogMessage);
137 MemoryProfiler::setLogCallback(
nullptr);
141 astView_->setModel(
nullptr);
148 if (astWorkerThread_) {
149 astWorkerThread_->quit();
150 astWorkerThread_->wait();
152 if (astExportThread_) {
153 astExportThread_->quit();
154 astExportThread_->wait();
159void MainWindow::applyUnifiedSelectionPalette() {
160 QPalette pal = QApplication::palette();
161 const QBrush activeHighlight =
162 pal.brush(QPalette::Active, QPalette::Highlight);
163 const QBrush activeText =
164 pal.brush(QPalette::Active, QPalette::HighlightedText);
165 pal.setBrush(QPalette::Inactive, QPalette::Highlight, activeHighlight);
166 pal.setBrush(QPalette::Inactive, QPalette::HighlightedText, activeText);
167 QApplication::setPalette(pal);
170void MainWindow::setupViewMenuDockActions() {
175 viewMenu_->addSeparator();
177 const std::initializer_list<std::pair<QDockWidget *, QString>> docks = {
178 {tuDock_, tr(
"File Explorer")},
179 {sourceDock_, tr(
"Source Code")},
180 {astDock_, tr(
"AST")},
181 {declContextDock_, tr(
"Declaration Context")},
182 {logDock_, tr(
"Logs")}};
183 for (
const auto &[dock, text] : docks) {
185 QAction *action = dock->toggleViewAction();
186 action->setText(text);
187 viewMenu_->addAction(action);
191 viewMenu_->addSeparator();
193 resetLayoutAction_ =
new QAction(tr(
"Reset Layout"),
this);
194 resetLayoutAction_->setStatusTip(tr(
"Restore the default dock layout"));
195 connect(resetLayoutAction_, &QAction::triggered,
this,
196 &MainWindow::onResetLayout);
197 viewMenu_->addAction(resetLayoutAction_);
200void MainWindow::setupUI() {
201 applyUnifiedSelectionPalette();
203 setDockNestingEnabled(
true);
204 setDockOptions(QMainWindow::AnimatedDocks | QMainWindow::AllowNestedDocks |
205 QMainWindow::AllowTabbedDocks);
208 setCentralWidget(
nullptr);
211void MainWindow::setupMenuBar() {
213 fileMenu_ = menuBar()->addMenu(tr(
"&File"));
216 openAction_ =
new QAction(tr(
"Open &Project..."),
this);
217 openAction_->setShortcut(QKeySequence::Open);
218 openAction_->setStatusTip(tr(
"Open a compilation database file"));
219 fileMenu_->addAction(openAction_);
220 settingsAction_ =
new QAction(tr(
"Open Config File..."),
this);
221 settingsAction_->setShortcut(QKeySequence::Preferences);
222 settingsAction_->setStatusTip(
223 tr(
"Open configuration file in external editor"));
224 fileMenu_->addAction(settingsAction_);
225 connect(settingsAction_, &QAction::triggered,
this,
226 &MainWindow::onOpenConfigFile);
227 reloadConfigAction_ =
new QAction(tr(
"Reload Configuration"),
this);
228 reloadConfigAction_->setShortcut(
229 QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_R));
230 reloadConfigAction_->setStatusTip(
231 tr(
"Reload configuration file and reapply settings"));
232 fileMenu_->addAction(reloadConfigAction_);
233 connect(reloadConfigAction_, &QAction::triggered,
this,
234 &MainWindow::onReloadConfig);
236 fileMenu_->addSeparator();
239 exitAction_ =
new QAction(tr(
"E&xit"),
this);
240 exitAction_->setShortcut(QKeySequence::Quit);
241 exitAction_->setStatusTip(tr(
"Exit the application"));
242 fileMenu_->addAction(exitAction_);
245 viewMenu_ = menuBar()->addMenu(tr(
"&View"));
248 navBackAction_ =
new QAction(tr(
"Back"),
this);
249 navBackAction_->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_BracketLeft));
250 navBackAction_->setShortcutContext(Qt::ApplicationShortcut);
251 navBackAction_->setEnabled(
false);
252 navForwardAction_ =
new QAction(tr(
"Forward"),
this);
253 navForwardAction_->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_BracketRight));
254 navForwardAction_->setShortcutContext(Qt::ApplicationShortcut);
255 navForwardAction_->setEnabled(
false);
256 addAction(navBackAction_);
257 addAction(navForwardAction_);
258 connect(navBackAction_, &QAction::triggered,
this,
259 [
this]() { navigateHistory(-1); });
260 connect(navForwardAction_, &QAction::triggered,
this,
261 [
this]() { navigateHistory(1); });
263 goToMacroDefinitionAction_ =
new QAction(tr(
"Go to Macro Definition"),
this);
264 goToMacroDefinitionAction_->setShortcut(
265 QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_M));
266 goToMacroDefinitionAction_->setShortcutContext(Qt::ApplicationShortcut);
267 goToMacroDefinitionAction_->setEnabled(
false);
268 addAction(goToMacroDefinitionAction_);
269 viewMenu_->addAction(goToMacroDefinitionAction_);
270 connect(goToMacroDefinitionAction_, &QAction::triggered,
this,
271 [
this]() { onGoToMacroDefinition(); });
273 zoomInAction_ =
new QAction(tr(
"Zoom In"),
this);
274 zoomInAction_->setShortcuts({QKeySequence(Qt::CTRL | Qt::Key_Plus),
275 QKeySequence(Qt::CTRL | Qt::Key_Equal)});
276 zoomInAction_->setShortcutContext(Qt::ApplicationShortcut);
277 addAction(zoomInAction_);
278 viewMenu_->addAction(zoomInAction_);
279 connect(zoomInAction_, &QAction::triggered,
this,
280 [
this]() { adjustFontSize(1); });
282 zoomOutAction_ =
new QAction(tr(
"Zoom Out"),
this);
283 zoomOutAction_->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Minus));
284 zoomOutAction_->setShortcutContext(Qt::ApplicationShortcut);
285 addAction(zoomOutAction_);
286 viewMenu_->addAction(zoomOutAction_);
287 connect(zoomOutAction_, &QAction::triggered,
this,
288 [
this]() { adjustFontSize(-1); });
290 zoomResetAction_ =
new QAction(tr(
"Reset Zoom"),
this);
291 zoomResetAction_->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_0));
292 zoomResetAction_->setShortcutContext(Qt::ApplicationShortcut);
293 addAction(zoomResetAction_);
294 viewMenu_->addAction(zoomResetAction_);
295 connect(zoomResetAction_, &QAction::triggered,
this, [
this]() {
296 int defaultSize = AppConfig::instance().getFontSize();
297 currentFontFamily_ = AppConfig::instance().getFontFamily();
298 QFont baseFont = QApplication::font();
299 if (!currentFontFamily_.isEmpty()) {
300 baseFont.setFamily(currentFontFamily_);
302 baseFont.setPointSize(defaultSize);
305 if (focusedDock_ == tuDock_ && tuView_) {
306 tuFontSize_ = defaultSize;
307 tuView_->setFont(baseFont);
308 }
else if (focusedDock_ == sourceDock_ && sourceView_) {
309 sourceFontSize_ = defaultSize;
310 sourceView_->setFont(baseFont);
311 sourceView_->applyFontSize(defaultSize);
312 }
else if (focusedDock_ == astDock_ && astView_) {
313 astFontSize_ = defaultSize;
314 astView_->setFont(baseFont);
315 if (astSearchQuickInput_) {
316 astSearchQuickInput_->setFont(baseFont);
318 if (astSearchPopup_) {
319 astSearchPopup_->setFont(baseFont);
321 if (astSearchCompleter_ && astSearchCompleter_->popup()) {
322 astSearchCompleter_->popup()->setFont(baseFont);
324 }
else if (focusedDock_ == declContextDock_ && declContextView_) {
325 declContextFontSize_ = defaultSize;
326 declContextView_->applyFont(baseFont);
327 }
else if (focusedDock_ == logDock_) {
328 logFontSize_ = defaultSize;
329 logDock_->applyFont(baseFont);
332 tuFontSize_ = defaultSize;
333 sourceFontSize_ = defaultSize;
334 astFontSize_ = defaultSize;
335 declContextFontSize_ = defaultSize;
336 logFontSize_ = defaultSize;
337 applyFontSize(defaultSize);
342 navToolBar_ = addToolBar(tr(
"Navigation"));
343 navToolBar_->addAction(navBackAction_);
344 navToolBar_->addAction(navForwardAction_);
345 navToolBar_->setMovable(
false);
348 ::QMenu *helpMenu = menuBar()->addMenu(tr(
"&Help"));
350 QAction *shortcutsAction =
new QAction(tr(
"&Keyboard Shortcuts"),
this);
351 shortcutsAction->setStatusTip(tr(
"Show keyboard shortcuts"));
352 connect(shortcutsAction, &QAction::triggered,
this, [
this]() {
353 auto *dialog =
new QDialog(
this);
354 dialog->setWindowTitle(tr(
"Keyboard Shortcuts"));
355 dialog->setAttribute(Qt::WA_DeleteOnClose);
357 QColor headerBg = palette().color(QPalette::Highlight);
358 QColor headerFg = palette().color(QPalette::HighlightedText);
360 auto makeTable = [&](
const QString &title) {
361 auto *group =
new QGroupBox(title, dialog);
362 auto *table =
new QTableWidget(group);
363 table->setColumnCount(2);
364 table->setHorizontalHeaderLabels({tr(
"Shortcut"), tr(
"Action")});
365 table->horizontalHeader()->setStretchLastSection(
true);
366 table->verticalHeader()->setVisible(
false);
367 table->setEditTriggers(QAbstractItemView::NoEditTriggers);
368 table->setSelectionMode(QAbstractItemView::NoSelection);
369 table->setFocusPolicy(Qt::NoFocus);
370 table->setShowGrid(
false);
371 table->setAlternatingRowColors(
true);
372 auto *layout =
new QVBoxLayout(group);
373 layout->setContentsMargins(0, 0, 0, 0);
374 layout->addWidget(table);
375 return std::make_pair(group, table);
378 auto addCategory = [&](QTableWidget *table,
const QString &name) {
379 int row = table->rowCount();
380 table->insertRow(row);
381 auto *item =
new QTableWidgetItem(name);
382 QFont f = item->font();
385 item->setBackground(headerBg);
386 item->setForeground(headerFg);
387 table->setItem(row, 0, item);
388 auto *spacer =
new QTableWidgetItem();
389 spacer->setBackground(headerBg);
390 table->setItem(row, 1, spacer);
391 table->setSpan(row, 0, 1, 2);
394 auto addShortcut = [](QTableWidget *table,
const QString &key,
395 const QString &action) {
396 int row = table->rowCount();
397 table->insertRow(row);
398 auto *keyItem =
new QTableWidgetItem(key);
399 QFont f = keyItem->font();
402 table->setItem(row, 0, keyItem);
403 table->setItem(row, 1,
new QTableWidgetItem(action));
406 auto finishTable = [](QTableWidget *table) {
407 table->resizeColumnsToContents();
408 table->resizeRowsToContents();
409 table->setMinimumWidth(table->columnWidth(0) + table->columnWidth(1) +
414 auto [leftGroup, leftTable] = makeTable(tr(
""));
415 addCategory(leftTable, tr(
"Focus Switching"));
416 addShortcut(leftTable,
"Tab", tr(
"Cycle focus between panes"));
417 addShortcut(leftTable,
"Ctrl+1", tr(
"File Explorer"));
418 addShortcut(leftTable,
"Ctrl+2", tr(
"Source Code"));
419 addShortcut(leftTable,
"Ctrl+3", tr(
"AST"));
420 addShortcut(leftTable,
"Ctrl+4", tr(
"Decl Context (Semantic)"));
421 addShortcut(leftTable,
"Ctrl+5", tr(
"Decl Context (Lexical)"));
422 addShortcut(leftTable,
"Ctrl+6", tr(
"Logs"));
423 addCategory(leftTable, tr(
"Tree Views"));
424 addShortcut(leftTable,
"Ctrl+Shift+E", tr(
"Expand all children"));
425 addShortcut(leftTable,
"Ctrl+Shift+C", tr(
"Collapse all children"));
426 addShortcut(leftTable,
"F5", tr(
"Extract AST for selected file"));
427 addCategory(leftTable, tr(
"Search"));
428 addShortcut(leftTable,
"Ctrl+F", tr(
"Focus search in active panel"));
429 addCategory(leftTable, tr(
"Source Code"));
430 addShortcut(leftTable,
"Home", tr(
"Go to start of file"));
431 addShortcut(leftTable,
"End", tr(
"Go to end of file"));
432 finishTable(leftTable);
435 auto [rightGroup, rightTable] = makeTable(tr(
""));
436 addCategory(rightTable, tr(
"View"));
437 addShortcut(rightTable,
"Ctrl++", tr(
"Zoom in"));
438 addShortcut(rightTable,
"Ctrl+-", tr(
"Zoom out"));
439 addShortcut(rightTable,
"Ctrl+0", tr(
"Reset zoom"));
440 addCategory(rightTable, tr(
"File"));
441 addShortcut(rightTable,
"Ctrl+O", tr(
"Open project"));
442 addShortcut(rightTable,
"Ctrl+Shift+R", tr(
"Reload configuration"));
443 addShortcut(rightTable,
"Ctrl+Q", tr(
"Quit"));
444 addCategory(rightTable, tr(
"Navigation"));
445 addShortcut(rightTable,
"Ctrl+[", tr(
"Navigate back"));
446 addShortcut(rightTable,
"Ctrl+]", tr(
"Navigate forward"));
447 addCategory(rightTable, tr(
"AST"));
448 addShortcut(rightTable,
"Ctrl+Shift+M", tr(
"Go to macro definition"));
449 addShortcut(rightTable,
"Ctrl+I", tr(
"Inspect node details"));
450 finishTable(rightTable);
452 auto *columnsLayout =
new QHBoxLayout;
453 columnsLayout->addWidget(leftGroup);
454 columnsLayout->addWidget(rightGroup);
456 auto *closeButton =
new QPushButton(tr(
"Close"), dialog);
457 connect(closeButton, &QPushButton::clicked, dialog, &QDialog::close);
459 auto *buttonLayout =
new QHBoxLayout;
460 buttonLayout->addStretch();
461 buttonLayout->addWidget(closeButton);
463 auto *mainLayout =
new QVBoxLayout(dialog);
464 mainLayout->addLayout(columnsLayout);
465 mainLayout->addLayout(buttonLayout);
467 dialog->resize(680, 480);
470 helpMenu->addAction(shortcutsAction);
472 helpMenu->addSeparator();
474 QAction *aboutAction =
new QAction(tr(
"&About"),
this);
475 aboutAction->setStatusTip(tr(
"About this application"));
476 connect(aboutAction, &QAction::triggered,
this, &MainWindow::onShowAbout);
477 helpMenu->addAction(aboutAction);
480void MainWindow::onOpenConfigFile() {
481 QString configPath = AppConfig::instance().getConfigFilePath();
483 QFileInfo fileInfo(configPath);
484 if (!fileInfo.exists()) {
485 QMessageBox::warning(
this, tr(
"Config File Not Found"),
486 tr(
"Configuration file not found:\n%1\n\n"
487 "The file should have been created automatically. "
488 "Try restarting the application.")
494 QUrl fileUrl = QUrl::fromLocalFile(configPath);
495 if (!QDesktopServices::openUrl(fileUrl)) {
496 QMessageBox::information(
497 this, tr(
"Open Config File"),
498 tr(
"Could not open the configuration file automatically.\n\n"
499 "Please open it manually with a text editor:\n%1")
502 logStatus(LogLevel::Info,
503 tr(
"Opened config file: %1").arg(fileInfo.fileName()));
507void MainWindow::onReloadConfig() {
508 AppConfig &config = AppConfig::instance();
509 const bool configAvailable = config.reload();
511 applyFontSize(config.getFontSize());
513 if (parallelQueryRunner_) {
514 parallelQueryRunner_->setParallelCount(config.getParallelProcessorCount());
516 if (astExtractorRunner_) {
517 astExtractorRunner_->setCommentExtractionEnabled(
518 config.getCommentExtractionEnabled());
521 if (!configAvailable) {
522 logStatus(LogLevel::Warning,
523 tr(
"Config file missing; created defaults and reloaded."));
527 logStatus(LogLevel::Info,
528 tr(
"Reloaded configuration: %1").arg(config.getConfigFilePath()));
531void MainWindow::onResetLayout() {
532 if (defaultDockState_.isEmpty()) {
535 restoreState(defaultDockState_);
538void MainWindow::onFocusChanged(QWidget *old, QWidget *now) {
542 const std::initializer_list<std::pair<QDockWidget *, DockTitleBar *>>
543 dockTitleBars = {{tuDock_, tuTitleBar_},
544 {sourceDock_, sourceTitleBar_},
545 {astDock_, astTitleBar_},
546 {declContextDock_, declContextTitleBar_},
547 {logDock_, logTitleBar_}};
550 QDockWidget *activeDock =
nullptr;
551 if (now && astSearchPopup_ && astSearchPopup_->isAncestorOf(now)) {
552 activeDock = astDock_;
554 for (
const auto &[dock, titleBar] : dockTitleBars) {
558 if (now && dock && dock->isAncestorOf(now)) {
564 if (focusedDock_ != activeDock) {
566 for (
const auto &[dock, titleBar] : dockTitleBars) {
568 titleBar->setFocused(dock == activeDock);
571 focusedDock_ = activeDock;
575void MainWindow::resizeEvent(QResizeEvent *event) {
576 QMainWindow::resizeEvent(event);
577 if (astSearchPopup_ && astSearchPopup_->isVisible()) {
578 syncAstSearchPopupGeometry();
582void MainWindow::moveEvent(QMoveEvent *event) {
583 QMainWindow::moveEvent(event);
584 if (astSearchPopup_ && astSearchPopup_->isVisible()) {
585 syncAstSearchPopupGeometry();
589bool MainWindow::eventFilter(QObject *watched, QEvent *event) {
590 if (watched == astDock_ && astSearchPopup_ && astSearchPopup_->isVisible()) {
591 switch (event->type()) {
596 syncAstSearchPopupGeometry();
602 return QMainWindow::eventFilter(watched, event);
605void MainWindow::onShowAbout() {
607 tr(
"<h2>ACAV (Clang AST Viewer)</h2>"
608 "<p><b>Version:</b> %1</p>"
609 "<p><b>Organization:</b> University of Victoria</p>"
610 "<p><b>Supervisor:</b> Professor Michael Adams</p>"
611 "<p><b>Developer:</b> Min Liu</p>"
613 "<p>A tool for visualizing and exploring Clang Abstract Syntax Trees "
615 "with support for C++20 modules.</p>"
616 "<p><b>Features:</b></p>"
618 "<li>Load and display AST from compilation databases</li>"
619 "<li>Bidirectional navigation between source code and AST</li>"
620 "<li>Support for C++20 modules</li>"
622 "<p>Built with Qt and LLVM/Clang.</p>")
623 .arg(ACAV_VERSION_STRING);
625 QMessageBox::about(
this, tr(
"About ACAV"), aboutText);
630class HistoryLineEdit :
public QLineEdit {
632 explicit HistoryLineEdit(QWidget *parent =
nullptr) : QLineEdit(parent) {}
635 void focusInEvent(QFocusEvent *event)
override {
636 QLineEdit::focusInEvent(event);
640 void mousePressEvent(QMouseEvent *event)
override {
641 QLineEdit::mousePressEvent(event);
642 if (event->button() == Qt::LeftButton) {
648 void showHistoryPopup() {
649 QCompleter *historyCompleter = completer();
650 if (!historyCompleter) {
653 historyCompleter->setCompletionPrefix(text().trimmed());
654 historyCompleter->complete();
658AcavJson sourceLocationToJson(
const SourceLocation &loc,
659 const FileManager &fileManager) {
661 if (!loc.isValid()) {
662 obj[
"valid"] =
false;
667 obj[
"fileId"] = loc.fileID();
668 obj[
"line"] = loc.line();
669 obj[
"column"] = loc.column();
671 std::string_view filePath = fileManager.getFilePath(loc.fileID());
672 if (!filePath.empty()) {
673 obj[
"filePath"] = InternedString(std::string(filePath));
679AcavJson sourceRangeToJson(
const SourceRange &range,
680 const FileManager &fileManager) {
682 obj[
"begin"] = sourceLocationToJson(range.begin(), fileManager);
683 obj[
"end"] = sourceLocationToJson(range.end(), fileManager);
687bool parseSourceLocationJson(
const AcavJson &obj, SourceLocation *out) {
688 if (!out || !obj.is_object()) {
691 auto fileIdIt = obj.find(
"fileId");
692 auto lineIt = obj.find(
"line");
693 auto columnIt = obj.find(
"column");
694 if (fileIdIt == obj.end() || lineIt == obj.end() || columnIt == obj.end()) {
697 if (!fileIdIt->is_number_integer() || !lineIt->is_number_integer() ||
698 !columnIt->is_number_integer()) {
702 FileID fileId =
static_cast<FileID>(fileIdIt->get<uint64_t>());
703 unsigned line =
static_cast<unsigned>(lineIt->get<uint64_t>());
704 unsigned column =
static_cast<unsigned>(columnIt->get<uint64_t>());
705 if (fileId == FileManager::InvalidFileID || line == 0 || column == 0) {
709 *out = SourceLocation(fileId, line, column);
713bool parseSourceRangeJson(
const AcavJson &obj, SourceRange *out) {
714 if (!out || !obj.is_object()) {
717 auto beginIt = obj.find(
"begin");
718 auto endIt = obj.find(
"end");
719 if (beginIt == obj.end() || endIt == obj.end()) {
722 SourceLocation begin(FileManager::InvalidFileID, 0, 0);
723 SourceLocation end(FileManager::InvalidFileID, 0, 0);
724 if (!parseSourceLocationJson(*beginIt, &begin) ||
725 !parseSourceLocationJson(*endIt, &end)) {
728 *out = SourceRange(begin, end);
732AcavJson buildAstJsonTree(AstViewNode *root,
const FileManager &fileManager) {
737 auto makeNodeJson = [&fileManager](AstViewNode *node) {
739 obj[
"properties"] = node->getProperties();
740 obj[
"sourceRange"] = sourceRangeToJson(node->getSourceRange(), fileManager);
744 AcavJson rootJson = makeNodeJson(root);
745 std::vector<std::pair<AstViewNode *, AcavJson *>> stack;
747 stack.push_back({root, &rootJson});
749 while (!stack.empty()) {
750 auto [node, jsonPtr] = stack.back();
753 const auto &children = node->getChildren();
754 AcavJson childArray = AcavJson::array();
755 childArray.get_ref<AcavJson::array_t &>().reserve(children.size());
756 (*jsonPtr)[
"children"] = std::move(childArray);
757 auto &storedChildren = (*jsonPtr)[
"children"];
759 for (AstViewNode *child : children) {
763 AcavJson childJson = makeNodeJson(child);
764 storedChildren.push_back(childJson);
765 stack.push_back({child, &storedChildren.back()});
774void MainWindow::setupDockWidgets() {
777 tuView_ =
new QTreeView(
this);
778 sourceView_ =
new SourceCodeView(
this);
779 astView_ =
new QTreeView(
this);
782 tuDock_ =
new QDockWidget(
this);
783 tuDock_->setObjectName(
"tuDock");
784 tuDock_->setAllowedAreas(Qt::AllDockWidgetAreas);
785 tuTitleBar_ =
new DockTitleBar(tr(
"File Explorer"), tuDock_);
786 tuDock_->setTitleBarWidget(tuTitleBar_);
788 sourceDock_ =
new QDockWidget(
this);
789 sourceDock_->setObjectName(
"sourceDock");
790 sourceDock_->setAllowedAreas(Qt::AllDockWidgetAreas);
791 sourceTitleBar_ =
new DockTitleBar(tr(
"Source Code"), sourceDock_);
792 sourceDock_->setTitleBarWidget(sourceTitleBar_);
793 QWidget *sourceContainer =
new QWidget(sourceDock_);
794 setupSourceSearchPanel(sourceContainer);
795 sourceDock_->setWidget(sourceContainer);
797 astDock_ =
new QDockWidget(
this);
798 astDock_->setObjectName(
"astDock");
799 astDock_->setAllowedAreas(Qt::AllDockWidgetAreas);
800 astTitleBar_ =
new DockTitleBar(tr(
"AST"), astDock_);
801 astDock_->setTitleBarWidget(astTitleBar_);
802 QWidget *astContainer =
new QWidget(astDock_);
803 setupAstSearchPanel(astContainer);
804 astDock_->setWidget(astContainer);
809 declContextDock_ =
new QDockWidget(
this);
810 declContextDock_->setObjectName(
"declContextDock");
811 declContextDock_->setAllowedAreas(Qt::AllDockWidgetAreas);
812 declContextTitleBar_ =
813 new DockTitleBar(tr(
"Declaration Context"), declContextDock_);
814 declContextDock_->setTitleBarWidget(declContextTitleBar_);
815 declContextView_ =
new DeclContextView(
this);
816 declContextDock_->setWidget(declContextView_);
818 logDock_ =
new LogDock(
this);
819 logDock_->setAllowedAreas(Qt::AllDockWidgetAreas);
820 logTitleBar_ =
new DockTitleBar(tr(
"Logs"), logDock_);
821 logDock_->setTitleBarWidget(logTitleBar_);
822 logDock_->setFeatures(logDock_->features() | QDockWidget::DockWidgetMovable |
823 QDockWidget::DockWidgetFloatable);
824 QPointer<LogDock> logDockPtr = logDock_;
825 MemoryProfiler::setLogCallback([logDockPtr](
const QString &message) {
830 entry.level = LogLevel::Debug;
831 entry.source = QStringLiteral(
"acav-memory");
832 entry.message = message;
833 entry.timestamp = QDateTime::currentDateTime();
834 QMetaObject::invokeMethod(logDockPtr,
"enqueue", Qt::QueuedConnection,
835 Q_ARG(LogEntry, entry));
840 addDockWidget(Qt::TopDockWidgetArea, tuDock_);
841 addDockWidget(Qt::TopDockWidgetArea, sourceDock_);
842 addDockWidget(Qt::TopDockWidgetArea, astDock_);
843 addDockWidget(Qt::TopDockWidgetArea, declContextDock_);
844 addDockWidget(Qt::BottomDockWidgetArea, logDock_);
849 splitDockWidget(tuDock_, sourceDock_, Qt::Horizontal);
850 splitDockWidget(sourceDock_, astDock_, Qt::Horizontal);
851 splitDockWidget(astDock_, declContextDock_, Qt::Horizontal);
855 resizeDocks({tuDock_, sourceDock_, astDock_, declContextDock_},
856 {300, 450, 450, 200}, Qt::Horizontal);
859 QFont initialLogFont = logDock_->font();
860 const QString configuredFamily = AppConfig::instance().getFontFamily();
861 if (!configuredFamily.isEmpty()) {
862 initialLogFont.setFamily(configuredFamily);
864 initialLogFont.setPointSize(AppConfig::instance().getFontSize());
865 const int lineHeight = QFontMetrics(initialLogFont).lineSpacing();
866 const int logDockHeight =
868 const int topDockHeight =
869 900 - logDockHeight - 60;
870 resizeDocks({sourceDock_, logDock_}, {topDockHeight, logDockHeight},
873 setupViewMenuDockActions();
874 ::QTimer::singleShot(0,
this, [
this]() { defaultDockState_ = saveState(); });
877void MainWindow::setupSourceSearchPanel(QWidget *container) {
882 auto *outerLayout =
new QVBoxLayout(container);
883 outerLayout->setContentsMargins(4, 4, 4, 4);
884 outerLayout->setSpacing(4);
886 auto *controlsLayout =
new QHBoxLayout();
887 controlsLayout->setContentsMargins(0, 0, 0, 0);
888 controlsLayout->setSpacing(4);
890 sourceSearchInput_ =
new QLineEdit(container);
891 sourceSearchInput_->setPlaceholderText(tr(
"Search source code..."));
892 sourceSearchInput_->setClearButtonEnabled(
true);
893 sourceSearchInput_->setProperty(
"searchField",
true);
894 sourceSearchInput_->setMinimumHeight(28);
896 sourceSearchPrevButton_ =
new QToolButton(container);
897 sourceSearchPrevButton_->setText(tr(
"Prev"));
898 sourceSearchPrevButton_->setEnabled(
false);
899 sourceSearchPrevButton_->setProperty(
"searchButton",
true);
900 sourceSearchPrevButton_->setMinimumHeight(28);
902 sourceSearchNextButton_ =
new QToolButton(container);
903 sourceSearchNextButton_->setText(tr(
"Next"));
904 sourceSearchNextButton_->setEnabled(
false);
905 sourceSearchNextButton_->setProperty(
"searchButton",
true);
906 sourceSearchNextButton_->setMinimumHeight(28);
908 sourceSearchStatus_ =
new QLabel(container);
909 sourceSearchStatus_->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
910 sourceSearchStatus_->setMinimumWidth(80);
911 sourceSearchStatus_->setProperty(
"searchStatus",
true);
913 sourceSearchDebounce_ = new ::QTimer(container);
914 sourceSearchDebounce_->setSingleShot(
true);
915 sourceSearchDebounce_->setInterval(200);
917 controlsLayout->addWidget(sourceSearchInput_, 1);
918 controlsLayout->addWidget(sourceSearchPrevButton_);
919 controlsLayout->addWidget(sourceSearchNextButton_);
920 controlsLayout->addWidget(sourceSearchStatus_);
922 outerLayout->addLayout(controlsLayout);
923 outerLayout->addWidget(sourceView_, 1);
925 connect(sourceSearchInput_, &QLineEdit::returnPressed,
this,
926 &MainWindow::onSourceSearchFindNext);
927 connect(sourceSearchInput_, &QLineEdit::textChanged,
this,
928 &MainWindow::onSourceSearchTextChanged);
929 connect(sourceSearchDebounce_, &::QTimer::timeout,
this,
930 &MainWindow::onSourceSearchDebounced);
931 connect(sourceSearchPrevButton_, &QToolButton::clicked,
this,
932 &MainWindow::onSourceSearchFindPrevious);
933 connect(sourceSearchNextButton_, &QToolButton::clicked,
this,
934 &MainWindow::onSourceSearchFindNext);
937void MainWindow::setupAstSearchPanel(QWidget *container) {
942 auto *outerLayout =
new QVBoxLayout(container);
943 outerLayout->setContentsMargins(8, 8, 8, 8);
944 outerLayout->setSpacing(8);
947 auto *quickSearchFrame =
new QWidget(container);
948 quickSearchFrame->setObjectName(
"astQuickSearchFrame");
950 auto *quickLayout =
new QHBoxLayout(quickSearchFrame);
951 quickLayout->setContentsMargins(8, 6, 8, 6);
952 quickLayout->setSpacing(6);
954 astSearchQuickInput_ =
new QLineEdit(quickSearchFrame);
955 astSearchQuickInput_->setObjectName(
"astSearchQuickInput");
956 astSearchQuickInput_->setPlaceholderText(tr(
"Search AST nodes..."));
957 astSearchQuickInput_->setClearButtonEnabled(
true);
958 astSearchQuickInput_->setProperty(
"searchField",
true);
959 astSearchQuickInput_->setMinimumHeight(28);
961 auto *quickSearchButton =
new QToolButton(quickSearchFrame);
962 quickSearchButton->setObjectName(
"astSearchQuickButton");
963 quickSearchButton->setText(tr(
"Advanced"));
964 quickSearchButton->setToolTip(tr(
"Open advanced search options (Ctrl+F)"));
965 quickSearchButton->setProperty(
"searchButton",
true);
966 quickSearchButton->setMinimumHeight(28);
967 quickSearchButton->setMinimumWidth(90);
969 quickLayout->addWidget(astSearchQuickInput_, 1);
970 quickLayout->addWidget(quickSearchButton);
973 astSearchPopup_ =
new QDialog(
this);
974 astSearchPopup_->setObjectName(
"astSearchPopup");
975 astSearchPopup_->setWindowTitle(tr(
"AST Advanced Search"));
976 astSearchPopup_->setModal(
false);
977 astSearchPopup_->setWindowModality(Qt::NonModal);
978 astSearchPopup_->setMinimumSize(320, 180);
979 astSearchPopup_->resize(600, 220);
981 auto *popupLayout =
new QVBoxLayout(astSearchPopup_);
982 popupLayout->setContentsMargins(12, 12, 12, 12);
983 popupLayout->setSpacing(10);
986 auto *inputLabel =
new QLabel(tr(
"<b>Search Pattern:</b>"), astSearchPopup_);
987 popupLayout->addWidget(inputLabel);
989 astSearchInput_ =
new HistoryLineEdit(astSearchPopup_);
990 astSearchInput_->setObjectName(
"astSearchInput");
991 astSearchInput_->setPlaceholderText(
992 tr(
"e.g., kind:FunctionDecl name:main type:.*int.*"));
993 astSearchInput_->setClearButtonEnabled(
true);
994 astSearchInput_->setProperty(
"searchField",
true);
995 astSearchInput_->setMinimumHeight(32);
996 astSearchInput_->setToolTip(
997 tr(
"Use qualifiers like kind:, name:, type: for precise matching.\n"
998 "Supports regex patterns.\n"
999 "Examples: kind:FunctionDecl name:main type:.*int.*"));
1001 astSearchHistoryModel_ =
new QStringListModel(astSearchPopup_);
1002 astSearchCompleter_ =
new QCompleter(astSearchHistoryModel_, astSearchPopup_);
1003 astSearchCompleter_->setCaseSensitivity(Qt::CaseInsensitive);
1004 astSearchCompleter_->setCompletionMode(QCompleter::PopupCompletion);
1005 astSearchCompleter_->setFilterMode(Qt::MatchContains);
1006 astSearchInput_->setCompleter(astSearchCompleter_);
1008 popupLayout->addWidget(astSearchInput_);
1011 auto *separator =
new QFrame(astSearchPopup_);
1012 separator->setFrameShape(QFrame::HLine);
1013 separator->setFrameShadow(QFrame::Sunken);
1014 popupLayout->addWidget(separator);
1017 auto *controlsFrame =
new QWidget(astSearchPopup_);
1018 auto *popupControlsLayout =
new QHBoxLayout(controlsFrame);
1019 popupControlsLayout->setContentsMargins(0, 0, 0, 0);
1020 popupControlsLayout->setSpacing(8);
1023 astSearchProjectFilter_ =
new QCheckBox(tr(
"Project Files Only"), astSearchPopup_);
1024 astSearchProjectFilter_->setChecked(
true);
1025 astSearchProjectFilter_->setToolTip(tr(
"Restrict search to files within the project root"));
1027 popupControlsLayout->addWidget(astSearchProjectFilter_);
1028 popupControlsLayout->addStretch(1);
1031 astSearchPrevButton_ =
new QToolButton(astSearchPopup_);
1032 astSearchPrevButton_->setText(tr(
"Previous"));
1033 astSearchPrevButton_->setToolTip(tr(
"Find previous match (Shift+Enter)"));
1034 astSearchPrevButton_->setEnabled(
false);
1035 astSearchPrevButton_->setProperty(
"searchButton",
true);
1036 astSearchPrevButton_->setMinimumWidth(90);
1037 astSearchPrevButton_->setMinimumHeight(28);
1039 astSearchNextButton_ =
new QToolButton(astSearchPopup_);
1040 astSearchNextButton_->setText(tr(
"Next"));
1041 astSearchNextButton_->setToolTip(tr(
"Find next match (Enter)"));
1042 astSearchNextButton_->setEnabled(
false);
1043 astSearchNextButton_->setProperty(
"searchButton",
true);
1044 astSearchNextButton_->setMinimumWidth(90);
1045 astSearchNextButton_->setMinimumHeight(28);
1047 auto *astSearchButton =
new QToolButton(astSearchPopup_);
1048 astSearchButton->setText(tr(
"Search"));
1049 astSearchButton->setToolTip(tr(
"Execute search"));
1050 astSearchButton->setProperty(
"searchButton",
true);
1051 astSearchButton->setMinimumWidth(90);
1052 astSearchButton->setMinimumHeight(28);
1054 popupControlsLayout->addWidget(astSearchPrevButton_);
1055 popupControlsLayout->addWidget(astSearchNextButton_);
1056 popupControlsLayout->addWidget(astSearchButton);
1058 popupLayout->addWidget(controlsFrame);
1061 astSearchStatus_ =
new QLabel(astSearchPopup_);
1062 astSearchStatus_->setObjectName(
"astSearchStatus");
1063 astSearchStatus_->setAlignment(Qt::AlignCenter);
1064 astSearchStatus_->setMinimumHeight(24);
1065 astSearchStatus_->setProperty(
"searchStatus",
true);
1066 popupLayout->addWidget(astSearchStatus_);
1069 astCompilationWarningLabel_ =
new QLabel(container);
1070 astCompilationWarningLabel_->setObjectName(
"astCompilationWarningLabel");
1071 astCompilationWarningLabel_->setText(
1072 tr(
"<b>Compilation Errors Detected:</b> AST may be incomplete. "
1073 "See log for details."));
1074 astCompilationWarningLabel_->setTextFormat(Qt::RichText);
1075 astCompilationWarningLabel_->setWordWrap(
true);
1077 astCompilationWarningLabel_->setVisible(
false);
1079 outerLayout->addWidget(quickSearchFrame);
1080 outerLayout->addWidget(astCompilationWarningLabel_);
1081 outerLayout->addWidget(astView_, 1);
1083 connect(astSearchQuickInput_, &QLineEdit::textChanged,
this,
1084 [
this](
const QString &text) {
1085 if (astSearchInput_ && astSearchInput_->text() != text) {
1086 astSearchInput_->setText(text);
1089 auto triggerQuickSearch = [
this]() {
1090 if (!astSearchInput_ || !astSearchQuickInput_) {
1093 const QString query = astSearchQuickInput_->text();
1094 if (astSearchInput_->text() != query) {
1095 astSearchInput_->setText(query);
1097 showAstSearchPopup(
false);
1098 onAstSearchFindNext();
1100 connect(astSearchQuickInput_, &QLineEdit::returnPressed,
this,
1101 triggerQuickSearch);
1102 connect(quickSearchButton, &QToolButton::clicked,
this, triggerQuickSearch);
1103 connect(astSearchInput_, &QLineEdit::returnPressed,
this,
1104 &MainWindow::onAstSearchFindNext);
1105 connect(astSearchInput_, &QLineEdit::textChanged,
this,
1106 &MainWindow::onAstSearchTextChanged);
1107 connect(astSearchInput_, &QLineEdit::textChanged,
this,
1108 [
this](
const QString &text) {
1109 if (astSearchQuickInput_ && astSearchQuickInput_->text() != text) {
1110 astSearchQuickInput_->setText(text);
1113 connect(astSearchButton, &QToolButton::clicked,
this,
1114 &MainWindow::onAstSearchFindNext);
1115 connect(astSearchPrevButton_, &QToolButton::clicked,
this,
1116 &MainWindow::onAstSearchFindPrevious);
1117 connect(astSearchNextButton_, &QToolButton::clicked,
this,
1118 &MainWindow::onAstSearchFindNext);
1119 connect(astSearchProjectFilter_, &QCheckBox::toggled,
this,
1120 &MainWindow::clearAstSearchState);
1122 auto *prevShortcut =
1123 new QShortcut(QKeySequence(Qt::SHIFT | Qt::Key_Return), astSearchPopup_);
1124 connect(prevShortcut, &QShortcut::activated,
this,
1125 &MainWindow::onAstSearchFindPrevious);
1126 auto *prevNumpadShortcut =
1127 new QShortcut(QKeySequence(Qt::SHIFT | Qt::Key_Enter), astSearchPopup_);
1128 connect(prevNumpadShortcut, &QShortcut::activated,
this,
1129 &MainWindow::onAstSearchFindPrevious);
1130 auto *closeShortcut =
new QShortcut(QKeySequence(Qt::Key_Escape),
1132 connect(closeShortcut, &QShortcut::activated, astSearchPopup_,
1136 astDock_->installEventFilter(
this);
1140 QFont popupFont = QApplication::font();
1141 popupFont.setPointSize(AppConfig::instance().getFontSize());
1142 QString configuredFamily = AppConfig::instance().getFontFamily();
1143 if (!configuredFamily.isEmpty()) {
1144 popupFont.setFamily(configuredFamily);
1146 astSearchPopup_->setFont(popupFont);
1147 if (astSearchCompleter_ && astSearchCompleter_->popup()) {
1148 astSearchCompleter_->popup()->setFont(popupFont);
1151 astSearchPopup_->hide();
1154void MainWindow::setupTuSearch() {
1155 tuSearch_ =
new QLineEdit(
this);
1156 tuSearch_->setPlaceholderText(tr(
"Filter files..."));
1157 tuSearch_->setClearButtonEnabled(
true);
1158 tuSearch_->setProperty(
"searchField",
true);
1159 tuSearch_->setMinimumHeight(28);
1160 connect(tuSearch_, &QLineEdit::textChanged,
this,
1161 [
this](
const QString &text) {
1162 QString needle = text.trimmed();
1166 tuView_->setUpdatesEnabled(
false);
1169 std::function<bool(
const QModelIndex &)> filterRecursive =
1170 [&](
const QModelIndex &parent) ->
bool {
1171 bool anyChildVisible =
false;
1172 int rowCount = tuModel_->rowCount(parent);
1174 for (
int i = 0; i < rowCount; ++i) {
1175 QModelIndex idx = tuModel_->index(i, 0, parent);
1176 QString name = tuModel_->data(idx, Qt::DisplayRole).toString();
1179 bool matches = needle.isEmpty() ||
1180 name.contains(needle, Qt::CaseInsensitive);
1183 bool childVisible = filterRecursive(idx);
1186 bool visible = matches || childVisible;
1187 tuView_->setRowHidden(i, parent, !visible);
1190 anyChildVisible =
true;
1194 return anyChildVisible;
1197 filterRecursive(QModelIndex());
1199 tuView_->setUpdatesEnabled(
true);
1203 QWidget *container =
new QWidget(tuDock_);
1204 QVBoxLayout *layout =
new QVBoxLayout(container);
1205 layout->setContentsMargins(4, 4, 4, 4);
1206 layout->setSpacing(4);
1207 layout->addWidget(tuSearch_);
1208 layout->addWidget(tuView_);
1209 tuDock_->setWidget(container);
1212void MainWindow::triggerSourceSearch(
bool forward) {
1213 if (!sourceView_ || !sourceSearchInput_) {
1217 const QString term = sourceSearchInput_->text();
1218 if (term.trimmed().isEmpty()) {
1219 if (sourceSearchStatus_) {
1220 sourceSearchStatus_->setText(tr(
"Enter text"));
1226 forward ? sourceView_->findNext(term) : sourceView_->findPrevious(term);
1227 if (sourceSearchStatus_) {
1228 sourceSearchStatus_->setText(found ? QString() : tr(
"No matches"));
1232void MainWindow::onSourceSearchTextChanged(
const QString &text) {
1233 const bool hasText = !text.trimmed().isEmpty();
1236 sourceSearchPrevButton_->setEnabled(hasText);
1237 sourceSearchNextButton_->setEnabled(hasText);
1238 sourceView_->clearSearchHighlight();
1239 sourceSearchStatus_->clear();
1242 sourceSearchDebounce_->start();
1244 sourceSearchDebounce_->stop();
1248void MainWindow::onSourceSearchDebounced() { triggerSourceSearch(
true); }
1250void MainWindow::onSourceSearchFindNext() {
1251 if (sourceSearchDebounce_) {
1252 sourceSearchDebounce_->stop();
1254 triggerSourceSearch(
true);
1257void MainWindow::onSourceSearchFindPrevious() {
1258 if (sourceSearchDebounce_) {
1259 sourceSearchDebounce_->stop();
1261 triggerSourceSearch(
false);
1264void MainWindow::onAstSearchTextChanged(
const QString &text) {
1265 const bool hasText = !text.trimmed().isEmpty();
1267 astSearchPrevButton_->setEnabled(hasText);
1268 astSearchNextButton_->setEnabled(hasText);
1269 clearAstSearchState();
1272void MainWindow::onAstSearchDebounced() { triggerAstSearch(
true); }
1274void MainWindow::onAstSearchFindNext() { triggerAstSearch(
true); }
1276void MainWindow::onAstSearchFindPrevious() { triggerAstSearch(
false); }
1278void MainWindow::triggerAstSearch(
bool forward) {
1279 if (!astSearchInput_) {
1283 const QString expression = astSearchInput_->text().trimmed();
1284 if (expression.isEmpty()) {
1287 rememberAstSearchQuery(expression);
1289 if (!astModel_ || !astModel_->hasNodes()) {
1290 setAstSearchStatus(tr(
"No AST loaded"),
true);
1294 if (astSearchMatches_.empty()) {
1295 collectAstSearchMatches(expression);
1297 if (astSearchMatches_.empty()) {
1298 if (astSearchStatus_ &&
1299 astSearchStatus_->text() != tr(
"Invalid pattern")) {
1300 setAstSearchStatus(tr(
"No matches"),
false);
1305 astSearchCurrentIndex_ =
1306 forward ? 0 :
static_cast<int>(astSearchMatches_.size()) - 1;
1308 int count =
static_cast<int>(astSearchMatches_.size());
1310 astSearchCurrentIndex_ = (astSearchCurrentIndex_ + 1) % count;
1312 astSearchCurrentIndex_ = (astSearchCurrentIndex_ - 1 + count) % count;
1316 navigateToAstMatch(astSearchCurrentIndex_);
1317 setAstSearchStatus(QString(
"%1 of %2")
1318 .arg(astSearchCurrentIndex_ + 1)
1319 .arg(astSearchMatches_.size()),
1323void MainWindow::collectAstSearchMatches(
const QString &expression) {
1324 astSearchMatches_.clear();
1325 astSearchCurrentIndex_ = -1;
1327 struct AstSearchCondition {
1329 QRegularExpression regex;
1333 std::vector<AstSearchCondition> conditions;
1334 QRegularExpression qualifierPattern(QStringLiteral(
"(\\w+):"));
1335 auto it = qualifierPattern.globalMatch(expression);
1338 struct QualifierPos {
1343 std::vector<QualifierPos> qualifiers;
1344 while (it.hasNext()) {
1345 auto match = it.next();
1346 qualifiers.push_back({
static_cast<int>(match.capturedStart()),
1347 static_cast<int>(match.capturedEnd()),
1348 match.captured(1)});
1351 if (qualifiers.empty()) {
1353 QRegularExpression regex(expression,
1354 QRegularExpression::CaseInsensitiveOption);
1355 if (!regex.isValid()) {
1356 setAstSearchStatus(tr(
"Invalid pattern"),
true);
1359 conditions.push_back({QString(), std::move(regex)});
1362 if (qualifiers.front().start > 0) {
1363 QString bareText = expression.left(qualifiers.front().start).trimmed();
1364 if (!bareText.isEmpty()) {
1365 QRegularExpression regex(bareText,
1366 QRegularExpression::CaseInsensitiveOption);
1367 if (!regex.isValid()) {
1368 setAstSearchStatus(tr(
"Invalid pattern"),
true);
1371 conditions.push_back({QString(), std::move(regex)});
1376 for (std::size_t i = 0; i < qualifiers.size(); ++i) {
1377 int valueStart = qualifiers[i].valueStart;
1378 int valueEnd = (i + 1 < qualifiers.size()) ? qualifiers[i + 1].start
1379 : expression.size();
1381 expression.mid(valueStart, valueEnd - valueStart).trimmed();
1385 value.isEmpty() ? value : QStringLiteral(
"^(?:%1)$").arg(value);
1386 QRegularExpression regex(anchored,
1387 QRegularExpression::CaseInsensitiveOption);
1388 if (!regex.isValid()) {
1389 setAstSearchStatus(tr(
"Invalid pattern"),
true);
1392 conditions.push_back({qualifiers[i].key, std::move(regex)});
1397 QString projectRoot = tuModel_ ? tuModel_->projectRoot() : QString();
1398 bool projectFilterActive =
1399 astSearchProjectFilter_ && astSearchProjectFilter_->isChecked();
1400 std::unordered_map<FileID, bool> projectFileCache;
1403 AstViewNode *root = astModel_->selectedNode();
1406 QModelIndex rootIndex = astModel_->index(0, 0);
1407 if (!rootIndex.isValid()) {
1410 root =
static_cast<AstViewNode *
>(
1411 rootIndex.data(AstModel::NodePtrRole).value<void *>());
1420 std::vector<AstViewNode *> stack;
1421 int topLevelCount = astModel_->rowCount();
1422 for (
int i = topLevelCount - 1; i >= 0; --i) {
1423 QModelIndex idx = astModel_->index(i, 0);
1424 if (idx.isValid()) {
1425 auto *node =
static_cast<AstViewNode *
>(
1426 idx.data(AstModel::NodePtrRole).value<void *>());
1428 stack.push_back(node);
1433 auto jsonValueToString = [](
const AcavJson &value) -> QString {
1434 if (value.is_boolean()) {
1435 return value.get<
bool>() ? QStringLiteral(
"true")
1436 : QStringLiteral(
"false");
1438 if (value.is_number_integer()) {
1439 return QString::number(value.get<int64_t>());
1441 if (value.is_number_unsigned()) {
1442 return QString::number(value.get<uint64_t>());
1444 if (value.is_number_float()) {
1445 return QString::number(value.get<
double>());
1447 if (value.is_string()) {
1448 return QString::fromStdString(value.get<InternedString>().str());
1453 while (!stack.empty()) {
1454 AstViewNode *node = stack.back();
1458 if (projectFilterActive) {
1459 FileID fileId = node->getSourceRange().begin().fileID();
1460 auto cacheIt = projectFileCache.find(fileId);
1461 if (cacheIt == projectFileCache.end()) {
1462 bool isProject =
false;
1463 if (fileId != FileManager::InvalidFileID && !projectRoot.isEmpty()) {
1464 std::string_view path = fileManager_.getFilePath(fileId);
1465 if (!path.empty()) {
1467 QString::fromUtf8(path.data(),
static_cast<int>(path.size()));
1468 isProject = (qpath == projectRoot) ||
1469 qpath.startsWith(projectRoot + QChar(
'/'));
1472 cacheIt = projectFileCache.emplace(fileId, isProject).first;
1474 if (!cacheIt->second) {
1476 const auto &children = node->getChildren();
1477 for (
auto childIt = children.rbegin(); childIt != children.rend();
1480 stack.push_back(*childIt);
1486 const auto &props = node->getProperties();
1487 bool allMatch =
true;
1489 for (
const auto &cond : conditions) {
1490 if (cond.key.isEmpty()) {
1492 bool anyPropMatch =
false;
1493 if (props.is_object()) {
1494 for (
auto it = props.begin(); it != props.end(); ++it) {
1495 QString val = jsonValueToString(*it);
1496 if (!val.isEmpty() && cond.regex.match(val).hasMatch()) {
1497 anyPropMatch =
true;
1502 if (!anyPropMatch) {
1508 QByteArray keyUtf8 = cond.key.toUtf8();
1509 const char *keyStr = keyUtf8.constData();
1510 auto propIt = props.find(keyStr);
1511 if (propIt == props.end()) {
1515 QString val = jsonValueToString(*propIt);
1516 if (!cond.regex.match(val).hasMatch()) {
1524 astSearchMatches_.push_back(node);
1528 const auto &children = node->getChildren();
1529 for (
auto childIt = children.rbegin(); childIt != children.rend();
1532 stack.push_back(*childIt);
1538void MainWindow::navigateToAstMatch(
int index) {
1539 if (index < 0 || index >=
static_cast<int>(astSearchMatches_.size())) {
1542 AstViewNode *node = astSearchMatches_[index];
1543 QModelIndex modelIndex = astModel_->selectNode(node);
1544 astView_->setCurrentIndex(modelIndex);
1545 astView_->scrollTo(modelIndex);
1548void MainWindow::clearAstSearchState() {
1549 astSearchMatches_.clear();
1550 astSearchCurrentIndex_ = -1;
1551 if (astSearchStatus_) {
1552 astSearchStatus_->clear();
1553 astSearchStatus_->setProperty(
"searchStatus",
true);
1554 astSearchStatus_->style()->unpolish(astSearchStatus_);
1555 astSearchStatus_->style()->polish(astSearchStatus_);
1559void MainWindow::setAstSearchStatus(
const QString &text,
bool isError) {
1560 if (!astSearchStatus_) {
1563 astSearchStatus_->setText(text);
1569 astSearchStatus_->setProperty(
"searchStatus", QStringLiteral(
"error"));
1571 astSearchStatus_->setProperty(
"searchStatus",
true);
1573 astSearchStatus_->style()->unpolish(astSearchStatus_);
1574 astSearchStatus_->style()->polish(astSearchStatus_);
1577void MainWindow::showAstSearchPopup(
bool selectAll) {
1578 if (!astSearchPopup_ || !astSearchInput_) {
1582 syncAstSearchPopupGeometry();
1583 astSearchPopup_->show();
1585 astSearchPopup_->raise();
1586 astSearchPopup_->activateWindow();
1587 astSearchInput_->setFocus();
1589 astSearchInput_->selectAll();
1593void MainWindow::syncAstSearchPopupGeometry() {
1594 if (!astSearchPopup_) {
1598 const int minWidth = astSearchPopup_->minimumWidth();
1599 const int maxWidth = 900;
1600 int targetWidth = astSearchPopup_->width();
1601 QPoint anchor(0, 0);
1604 const int availableWidth = qMax(minWidth, astDock_->width() - 16);
1605 const int preferredWidth =
static_cast<int>(availableWidth * 0.85);
1606 targetWidth = qBound(minWidth, preferredWidth, qMin(maxWidth, availableWidth));
1607 const int x = qMax(8, astDock_->width() - targetWidth - 8);
1608 anchor = astDock_->mapToGlobal(QPoint(x, 40));
1610 const int availableWidth = qMax(minWidth, width() - 32);
1611 const int preferredWidth =
static_cast<int>(availableWidth * 0.55);
1612 targetWidth = qBound(minWidth, preferredWidth, qMin(maxWidth, availableWidth));
1613 anchor = mapToGlobal(QPoint(qMax(8, width() - targetWidth - 16), 40));
1616 int targetHeight = qMax(astSearchPopup_->sizeHint().height(),
1617 astSearchPopup_->minimumHeight());
1618 astSearchPopup_->resize(targetWidth, targetHeight);
1619 astSearchPopup_->move(anchor);
1622void MainWindow::rememberAstSearchQuery(
const QString &query) {
1623 const QString trimmed = query.trimmed();
1624 if (trimmed.isEmpty()) {
1628 astSearchHistory_.removeAll(trimmed);
1629 astSearchHistory_.prepend(trimmed);
1631 constexpr int kMaxQueryHistory = 20;
1632 while (astSearchHistory_.size() > kMaxQueryHistory) {
1633 astSearchHistory_.removeLast();
1636 if (astSearchHistoryModel_) {
1637 astSearchHistoryModel_->setStringList(astSearchHistory_);
1641void MainWindow::setAstCompilationWarningVisible(
bool visible) {
1642 if (!astCompilationWarningLabel_) {
1645 astCompilationWarningLabel_->setVisible(visible);
1648void MainWindow::applyFontSize(
int size) {
1650 if (size < kMinFontSize) {
1651 size = kMinFontSize;
1652 }
else if (size > kMaxFontSize) {
1653 size = kMaxFontSize;
1655 currentFontSize_ = size;
1657 sourceFontSize_ = size;
1658 astFontSize_ = size;
1659 declContextFontSize_ = size;
1660 logFontSize_ = size;
1661 currentFontFamily_ = AppConfig::instance().getFontFamily();
1663 QFont baseFont = QApplication::font();
1664 if (!currentFontFamily_.isEmpty()) {
1665 baseFont.setFamily(currentFontFamily_);
1667 baseFont.setPointSize(size);
1669 auto applyFont = [&baseFont](QWidget *widget) {
1673 widget->setFont(baseFont);
1677 applyFont(astView_);
1678 if (declContextView_) {
1679 declContextView_->applyFont(baseFont);
1682 logDock_->applyFont(baseFont);
1684 applyFont(nodeCycleWidget_);
1685 applyFont(astSearchQuickInput_);
1686 applyFont(astSearchPopup_);
1687 if (astSearchCompleter_ && astSearchCompleter_->popup()) {
1688 astSearchCompleter_->popup()->setFont(baseFont);
1691 sourceView_->setFont(baseFont);
1692 sourceView_->applyFontSize(size);
1696void MainWindow::adjustFontSize(
int delta) {
1698 currentFontFamily_ = AppConfig::instance().getFontFamily();
1699 QFont baseFont = QApplication::font();
1700 if (!currentFontFamily_.isEmpty()) {
1701 baseFont.setFamily(currentFontFamily_);
1704 auto adjustWidget = [&](QWidget *widget,
int *fontSize) {
1705 if (!widget || !fontSize) {
1708 int nextSize = *fontSize + delta;
1709 if (nextSize < kMinFontSize) {
1710 nextSize = kMinFontSize;
1711 }
else if (nextSize > kMaxFontSize) {
1712 nextSize = kMaxFontSize;
1714 if (nextSize == *fontSize) {
1717 *fontSize = nextSize;
1718 baseFont.setPointSize(nextSize);
1719 widget->setFont(baseFont);
1722 if (focusedDock_ == tuDock_) {
1723 adjustWidget(tuView_, &tuFontSize_);
1724 }
else if (focusedDock_ == sourceDock_) {
1725 int nextSize = sourceFontSize_ + delta;
1726 if (nextSize < kMinFontSize) {
1727 nextSize = kMinFontSize;
1728 }
else if (nextSize > kMaxFontSize) {
1729 nextSize = kMaxFontSize;
1731 if (nextSize != sourceFontSize_ && sourceView_) {
1732 sourceFontSize_ = nextSize;
1733 baseFont.setPointSize(nextSize);
1734 sourceView_->setFont(baseFont);
1735 sourceView_->applyFontSize(nextSize);
1737 }
else if (focusedDock_ == astDock_) {
1738 adjustWidget(astView_, &astFontSize_);
1739 baseFont.setPointSize(astFontSize_);
1740 if (astSearchQuickInput_) {
1741 astSearchQuickInput_->setFont(baseFont);
1743 if (astSearchPopup_) {
1744 astSearchPopup_->setFont(baseFont);
1746 if (astSearchCompleter_ && astSearchCompleter_->popup()) {
1747 astSearchCompleter_->popup()->setFont(baseFont);
1749 }
else if (focusedDock_ == declContextDock_) {
1750 int nextSize = declContextFontSize_ + delta;
1751 if (nextSize < kMinFontSize) {
1752 nextSize = kMinFontSize;
1753 }
else if (nextSize > kMaxFontSize) {
1754 nextSize = kMaxFontSize;
1756 if (nextSize != declContextFontSize_ && declContextView_) {
1757 declContextFontSize_ = nextSize;
1758 baseFont.setPointSize(nextSize);
1759 declContextView_->applyFont(baseFont);
1761 }
else if (focusedDock_ == logDock_) {
1762 int nextSize = logFontSize_ + delta;
1763 if (nextSize < kMinFontSize) {
1764 nextSize = kMinFontSize;
1765 }
else if (nextSize > kMaxFontSize) {
1766 nextSize = kMaxFontSize;
1768 if (nextSize != logFontSize_ && logDock_) {
1769 logFontSize_ = nextSize;
1770 baseFont.setPointSize(nextSize);
1771 logDock_->applyFont(baseFont);
1775 int nextSize = currentFontSize_ + delta;
1776 if (nextSize < kMinFontSize) {
1777 nextSize = kMinFontSize;
1778 }
else if (nextSize > kMaxFontSize) {
1779 nextSize = kMaxFontSize;
1781 if (nextSize != currentFontSize_) {
1782 applyFontSize(nextSize);
1787void MainWindow::expandFileExplorerTopLevel() {
1788 if (!tuView_ || !tuModel_) {
1793 int topLevelCount = tuModel_->rowCount();
1794 for (
int i = 0; i < topLevelCount; ++i) {
1795 QModelIndex topLevelIndex = tuModel_->index(i, 0);
1796 tuView_->expand(topLevelIndex);
1800void MainWindow::setupModels() {
1802 tuModel_ =
new TranslationUnitModel(fileManager_,
this);
1803 astModel_ =
new AstModel(
this);
1806 tuView_->setModel(tuModel_);
1807 astView_->setModel(astModel_);
1810 auto configureTreeView = [](QTreeView *view) {
1811 view->setHeaderHidden(
true);
1812 view->setAnimated(
false);
1813 view->setUniformRowHeights(
true);
1814 view->setTextElideMode(Qt::ElideNone);
1815 view->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
1816 view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
1817 view->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
1818 view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
1819 view->header()->setStretchLastSection(
false);
1823 view->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
1825 configureTreeView(tuView_);
1826 configureTreeView(astView_);
1829 queryRunner_ =
new QueryDependenciesRunner(
this);
1830 parallelQueryRunner_ =
new QueryDependenciesParallelRunner(
this);
1833 parallelQueryRunner_->setParallelCount(
1834 AppConfig::instance().getParallelProcessorCount());
1837 makeAstRunner_ =
new MakeAstRunner(
this);
1841 if (!clangResourceDir.empty()) {
1842 QString dir = QString::fromStdString(clangResourceDir);
1843 queryRunner_->setClangResourceDir(dir);
1844 parallelQueryRunner_->setClangResourceDir(dir);
1845 makeAstRunner_->setClangResourceDir(dir);
1849 astContext_ = std::make_unique<AstContext>();
1851 applyFontSize(AppConfig::instance().getFontSize());
1854 astWorkerThread_ =
new QThread(
this);
1855 astExtractorRunner_ =
new AstExtractorRunner(astContext_.get(), fileManager_);
1856 astExtractorRunner_->setCommentExtractionEnabled(
1857 AppConfig::instance().getCommentExtractionEnabled());
1860 nodeCycleWidget_ =
new NodeCycleWidget(
this);
1861 astExtractorRunner_->moveToThread(astWorkerThread_);
1862 astWorkerThread_->start();
1865void MainWindow::connectSignals() {
1867 connect(openAction_, &QAction::triggered,
this,
1868 &MainWindow::onOpenCompilationDatabase);
1869 connect(exitAction_, &QAction::triggered,
this, &MainWindow::onExit);
1872 connect(qApp, &QApplication::focusChanged,
this, &MainWindow::onFocusChanged);
1875 auto addFocusShortcut = [
this](
const QKeySequence &key,
auto callback) {
1876 auto *action =
new QAction(
this);
1877 action->setShortcut(key);
1878 connect(action, &QAction::triggered,
this, callback);
1882 addFocusShortcut(Qt::CTRL | Qt::Key_1, [
this]() { tuView_->setFocus(); });
1883 addFocusShortcut(Qt::CTRL | Qt::Key_2, [
this]() { sourceView_->setFocus(); });
1884 addFocusShortcut(Qt::CTRL | Qt::Key_3, [
this]() { astView_->setFocus(); });
1885 addFocusShortcut(Qt::CTRL | Qt::Key_4,
1886 [
this]() { declContextView_->focusSemanticTree(); });
1887 addFocusShortcut(Qt::CTRL | Qt::Key_5,
1888 [
this]() { declContextView_->focusLexicalTree(); });
1889 addFocusShortcut(Qt::CTRL | Qt::Key_6, [
this]() { logDock_->focusAllTab(); });
1890 addFocusShortcut(QKeySequence::Find, [
this]() {
1891 if (focusedDock_ == tuDock_ && tuSearch_) {
1892 tuSearch_->setFocus();
1893 tuSearch_->selectAll();
1894 }
else if (focusedDock_ == astDock_ && astSearchQuickInput_) {
1895 astSearchQuickInput_->setFocus();
1896 astSearchQuickInput_->selectAll();
1897 }
else if (sourceSearchInput_) {
1898 sourceSearchInput_->setFocus();
1899 sourceSearchInput_->selectAll();
1904 addFocusShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_E, [
this]() {
1905 if (astView_->hasFocus()) {
1906 expandAllChildren(astView_);
1907 }
else if (tuView_->hasFocus()) {
1908 onExpandAllTuChildren();
1911 addFocusShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_C, [
this]() {
1912 if (astView_->hasFocus()) {
1913 collapseAllChildren(astView_);
1914 }
else if (tuView_->hasFocus()) {
1915 onCollapseAllTuChildren();
1920 addFocusShortcut(Qt::Key_F5, [
this]() {
1921 QModelIndex index = tuView_->currentIndex();
1922 if (index.isValid()) {
1923 onTranslationUnitSelected(index);
1928 addFocusShortcut(Qt::CTRL | Qt::Key_I, [
this]() {
1929 QModelIndex index = astView_->currentIndex();
1930 if (!index.isValid()) {
1933 auto *node =
static_cast<AstViewNode *
>(index.internalPointer());
1935 onViewNodeDetails(node);
1941 if (tuView_->selectionModel()) {
1942 connect(tuView_->selectionModel(), &QItemSelectionModel::currentChanged,
1943 this, [
this](
const QModelIndex ¤t,
const QModelIndex &) {
1944 onTranslationUnitClicked(current);
1947 connect(tuView_, &QTreeView::doubleClicked,
this,
1948 &MainWindow::onTranslationUnitSelected);
1951 connect(astView_->selectionModel(), &QItemSelectionModel::currentChanged,
1952 this, &MainWindow::onAstNodeSelected);
1955 astView_->selectionModel(), &QItemSelectionModel::currentChanged,
this,
1956 [
this](
const QModelIndex ¤t,
const QModelIndex &) {
1957 if (isNavigatingFromDeclContext_) {
1960 if (!current.isValid()) {
1961 declContextView_->clear();
1964 auto *node =
static_cast<AstViewNode *
>(current.internalPointer());
1965 declContextView_->setSelectedNode(node);
1968 connect(declContextView_, &DeclContextView::contextNodeClicked,
this,
1969 [
this](AstViewNode *node) {
1973 isNavigatingFromDeclContext_ =
true;
1974 QModelIndex modelIndex = astModel_->selectNode(node);
1975 if (modelIndex.isValid()) {
1976 astView_->selectionModel()->setCurrentIndex(
1977 modelIndex, QItemSelectionModel::ClearAndSelect);
1978 astView_->scrollTo(modelIndex);
1980 isNavigatingFromDeclContext_ =
false;
1982 astView_->setContextMenuPolicy(Qt::CustomContextMenu);
1983 connect(astView_, &QTreeView::customContextMenuRequested,
this,
1984 &MainWindow::onAstContextMenuRequested);
1985 tuView_->setContextMenuPolicy(Qt::CustomContextMenu);
1986 connect(tuView_, &QTreeView::customContextMenuRequested,
this,
1987 &MainWindow::onTuContextMenuRequested);
1988 connect(sourceView_, &SourceCodeView::sourcePositionClicked,
this,
1989 &MainWindow::onSourcePositionClicked);
1990 connect(sourceView_, &SourceCodeView::sourceRangeSelected,
this,
1991 &MainWindow::onSourceRangeSelected);
1992 connect(nodeCycleWidget_, &NodeCycleWidget::nodeSelected,
this,
1993 &MainWindow::onCycleNodeSelected);
1994 connect(nodeCycleWidget_, &NodeCycleWidget::closed,
this,
1995 &MainWindow::onCycleWidgetClosed);
1998 connect(queryRunner_, &QueryDependenciesRunner::dependenciesReady,
this,
1999 &MainWindow::onDependenciesReady);
2000 connect(queryRunner_, &QueryDependenciesRunner::dependenciesReadyWithErrors,
2001 this, &MainWindow::onDependenciesReadyWithErrors);
2002 connect(queryRunner_, &QueryDependenciesRunner::error,
this,
2003 &MainWindow::onDependenciesError);
2004 connect(queryRunner_, &QueryDependenciesRunner::progress,
this,
2005 &MainWindow::onDependenciesProgress);
2007 connect(queryRunner_, &QueryDependenciesRunner::logMessage, logDock_,
2012 connect(parallelQueryRunner_,
2013 &QueryDependenciesParallelRunner::dependenciesReady,
this,
2014 &MainWindow::onDependenciesReady);
2015 connect(parallelQueryRunner_,
2016 &QueryDependenciesParallelRunner::dependenciesReadyWithErrors,
this,
2017 &MainWindow::onDependenciesReadyWithErrors);
2018 connect(parallelQueryRunner_, &QueryDependenciesParallelRunner::error,
this,
2019 &MainWindow::onDependenciesError);
2020 connect(parallelQueryRunner_, &QueryDependenciesParallelRunner::progress,
2021 this, &MainWindow::onDependenciesProgress);
2023 connect(parallelQueryRunner_, &QueryDependenciesParallelRunner::logMessage,
2024 logDock_, &LogDock::enqueue);
2028 connect(makeAstRunner_, &MakeAstRunner::astReady,
this,
2029 &MainWindow::onAstReady);
2030 connect(makeAstRunner_, &MakeAstRunner::error,
this, &MainWindow::onAstError);
2031 connect(makeAstRunner_, &MakeAstRunner::progress,
this,
2032 &MainWindow::onAstProgress);
2033 connect(makeAstRunner_, &MakeAstRunner::logMessage,
this,
2034 &MainWindow::onMakeAstLogMessage);
2036 connect(makeAstRunner_, &MakeAstRunner::logMessage, logDock_,
2041 connect(astExtractorRunner_, &AstExtractorRunner::finished,
this,
2042 &MainWindow::onAstExtracted);
2043 connect(astExtractorRunner_, &AstExtractorRunner::error,
this,
2044 &MainWindow::onAstError);
2045 connect(astExtractorRunner_, &AstExtractorRunner::progress,
this,
2046 &MainWindow::onAstProgress);
2047 connect(astExtractorRunner_, &AstExtractorRunner::statsUpdated,
this,
2048 &MainWindow::onAstStatsUpdated);
2049 connect(astExtractorRunner_, &AstExtractorRunner::started,
this,
2050 &MainWindow::onAstProgress);
2052 connect(astExtractorRunner_, &AstExtractorRunner::logMessage, logDock_,
2053 &LogDock::enqueue, Qt::QueuedConnection);
2058 const QString &projectRoot) {
2063 QFileInfo info(compilationDatabasePath);
2064 compilationDatabasePath_ = info.absoluteFilePath();
2065 const QString normalizedCompilationDatabasePath = compilationDatabasePath_;
2070 if (projectRoot.isEmpty()) {
2071 projectRoot_ = QString();
2073 projectRoot_ = QFileInfo(projectRoot).absoluteFilePath();
2078 QString outputFilePath =
2086 std::string errorMsg;
2087 std::vector<std::string> sourceFiles =
2089 normalizedCompilationDatabasePath.toStdString(), errorMsg);
2093 auto dedupSources = [](
const std::vector<std::string> &paths) {
2094 std::vector<std::string> unique;
2095 unique.reserve(paths.size());
2096 std::unordered_set<std::string> seen;
2097 for (
const std::string &path : paths) {
2098 if (seen.insert(path).second) {
2099 unique.push_back(path);
2104 sourceFiles = dedupSources(sourceFiles);
2106 if (sourceFiles.empty()) {
2107 QMessageBox::critical(
this, tr(
"Error"),
2108 tr(
"Failed to load source files: %1")
2109 .arg(QString::fromStdString(errorMsg)));
2114 (
static_cast<int>(sourceFiles.size()) >= kParallelThreshold);
2117 logStatus(LogLevel::Info,
2118 QString(
"Starting parallel dependency analysis (%1 files)...")
2119 .arg(sourceFiles.size()),
2120 QStringLiteral(
"query-dependencies"));
2121 parallelQueryRunner_->run(normalizedCompilationDatabasePath,
2124 logStatus(LogLevel::Info,
2125 QString(
"Loading compilation database: %1\nCache directory: %2")
2126 .arg(normalizedCompilationDatabasePath)
2128 QStringLiteral(
"query-dependencies"));
2129 queryRunner_->run(normalizedCompilationDatabasePath, outputFilePath);
2133void MainWindow::onOpenCompilationDatabase() {
2134 if (isAstExportInProgress_) {
2135 logStatus(LogLevel::Info, tr(
"AST export in progress, please wait..."));
2139 OpenProjectDialog dialog(
this);
2140 if (dialog.exec() != QDialog::Accepted) {
2144 QString dbPath = dialog.compilationDatabasePath();
2145 QString projectRoot = dialog.projectRootPath();
2147 if (dbPath.isEmpty()) {
2151 loadCompilationDatabase(dbPath, projectRoot);
2154void MainWindow::onExit() { close(); }
2156void MainWindow::onTranslationUnitClicked(
const QModelIndex &index) {
2157 if (!index.isValid()) {
2161 QString filePath = tuModel_->getSourceFilePathFromIndex(index);
2162 if (filePath.isEmpty()) {
2166 if (filePath == sourceView_->currentFilePath()) {
2170 logStatus(LogLevel::Info, QString(
"Loading file: %1").arg(filePath));
2172 if (sourceView_->loadFile(filePath)) {
2174 FileID fileId = fileManager_.registerFile(filePath.toStdString());
2175 sourceView_->setCurrentFileId(fileId);
2176 updateSourceSubtitle(filePath);
2178 logStatus(LogLevel::Info, QString(
"Loaded: %1").arg(filePath));
2183 bool shouldClearAst =
false;
2184 if (isSourceFile(filePath)) {
2186 shouldClearAst = (filePath != currentSourceFilePath_);
2187 }
else if (astContext_) {
2189 const auto &index = astContext_->getLocationIndex();
2190 shouldClearAst = !index.hasFile(fileId);
2193 if (shouldClearAst) {
2195 declContextView_->clear();
2196 astHasCompilationErrors_ =
false;
2197 setAstCompilationWarningVisible(
false);
2200 sourceView_->setCurrentFileId(FileManager::InvalidFileID);
2201 logStatus(LogLevel::Error, QString(
"Failed to load: %1").arg(filePath));
2206void MainWindow::onTranslationUnitSelected(
const QModelIndex &index) {
2207 if (!index.isValid()) {
2211 QString filePath = tuModel_->getSourceFilePathFromIndex(index);
2212 if (filePath.isEmpty()) {
2216 MemoryProfiler::checkpoint(
"File double-clicked - before checks");
2219 if (filePath == currentSourceFilePath_ && astModel_->hasNodes()) {
2220 logStatus(LogLevel::Info,
2221 QString(
"AST already loaded for: %1").arg(filePath));
2226 if (!isSourceFile(filePath)) {
2227 logStatus(LogLevel::Warning, tr(
"Cannot load AST for header files"));
2229 if (sourceView_->loadFile(filePath)) {
2230 FileID fileId = fileManager_.registerFile(filePath.toStdString());
2231 sourceView_->setCurrentFileId(fileId);
2232 updateSourceSubtitle(filePath);
2233 highlightTuFile(fileId);
2239 if (isAstExtractionInProgress_) {
2241 QFileInfo currentInfo(pendingSourceFilePath_);
2244 QString(
"AST extraction already in progress for %1. Please wait...")
2245 .arg(currentInfo.fileName()));
2249 if (isAstExportInProgress_) {
2250 logStatus(LogLevel::Info, tr(
"AST export in progress, please wait..."));
2254 MemoryProfiler::checkpoint(
"Before clearing old AST");
2258 astHasCompilationErrors_ =
false;
2259 setAstCompilationWarningVisible(
false);
2261 MemoryProfiler::checkpoint(
"After clearing old AST model");
2265 MemoryProfiler::checkpoint(
"After clearHistory()");
2269 if (astExtractorRunner_) {
2271 astExtractorRunner_->disconnect();
2273 astExtractorRunner_->moveToThread(QThread::currentThread());
2274 delete astExtractorRunner_;
2275 astExtractorRunner_ =
nullptr;
2278 astContext_.reset();
2280 MemoryProfiler::checkpoint(
"After destroying old AST context");
2282 astContext_ = std::make_unique<AstContext>();
2285 MemoryProfiler::checkpoint(
"After creating new AST context");
2288 astExtractorRunner_ =
new AstExtractorRunner(astContext_.get(), fileManager_);
2289 astExtractorRunner_->setCommentExtractionEnabled(
2290 AppConfig::instance().getCommentExtractionEnabled());
2291 astExtractorRunner_->moveToThread(astWorkerThread_);
2294 connect(astExtractorRunner_, &AstExtractorRunner::finished,
this,
2295 &MainWindow::onAstExtracted);
2296 connect(astExtractorRunner_, &AstExtractorRunner::error,
this,
2297 &MainWindow::onAstError);
2298 connect(astExtractorRunner_, &AstExtractorRunner::progress,
this,
2299 &MainWindow::onAstProgress);
2300 connect(astExtractorRunner_, &AstExtractorRunner::statsUpdated,
this,
2301 &MainWindow::onAstStatsUpdated);
2302 connect(astExtractorRunner_, &AstExtractorRunner::started,
this,
2303 &MainWindow::onAstProgress);
2305 logStatus(LogLevel::Info, QString(
"Loading file: %1").arg(filePath));
2307 if (sourceView_->loadFile(filePath)) {
2309 FileID fileId = fileManager_.registerFile(filePath.toStdString());
2310 sourceView_->setCurrentFileId(fileId);
2311 updateSourceSubtitle(filePath);
2312 logStatus(LogLevel::Info, QString(
"Loaded: %1").arg(filePath));
2314 sourceView_->setCurrentFileId(FileManager::InvalidFileID);
2315 logStatus(LogLevel::Error, QString(
"Failed to load: %1").arg(filePath));
2319 MemoryProfiler::checkpoint(
"After loading source file to view");
2322 AppConfig &config = AppConfig::instance();
2323 QString astFilePath =
2324 config.getAstFilePath(compilationDatabasePath_, filePath);
2326 QFileInfo astFileInfo(astFilePath);
2328 if (!astFileInfo.exists()) {
2330 currentSourceFilePath_ = filePath;
2331 pendingSourceFilePath_ = filePath;
2332 isAstExtractionInProgress_ =
true;
2333 astHasCompilationErrors_ =
false;
2334 QFileInfo sourceInfo(filePath);
2335 logStatus(LogLevel::Info,
2336 "Generating AST for " + sourceInfo.fileName() +
"...");
2337 onTimingMessage(QString(
"AST input files: %1 (source + headers)")
2338 .arg(getFileListForSource(filePath).size()));
2339 MemoryProfiler::checkpoint(
"Before make-ast generation");
2340 makeAstRunner_->run(compilationDatabasePath_, filePath, astFilePath);
2342 currentSourceFilePath_ = filePath;
2343 pendingSourceFilePath_ = filePath;
2344 isAstExtractionInProgress_ =
true;
2345 astHasCompilationErrors_ = loadAstCompilationErrorState(astFilePath);
2346 onTimingMessage(QString(
"AST input files: %1 (source + headers)")
2347 .arg(getFileListForSource(filePath).size()));
2348 MemoryProfiler::checkpoint(
"Before AST extraction from cache");
2350 extractAst(astFilePath, filePath);
2354void MainWindow::onDependenciesReady(
const QJsonObject &dependencies) {
2355 tuModel_->populateFromDependencies(dependencies, projectRoot_,
2356 compilationDatabasePath_);
2357 expandFileExplorerTopLevel();
2360 QJsonObject stats = dependencies[
"statistics"].toObject();
2361 int fileCount = stats[
"successCount"].toInt();
2362 int totalHeaders = stats[
"totalHeaderCount"].toInt();
2364 logStatus(LogLevel::Info,
2365 QString(
"Loaded %1 translation units").arg(fileCount),
2366 QStringLiteral(
"query-dependencies"));
2367 onTimingMessage(QString(
"Dependencies summary: %1 sources, %2 headers")
2369 .arg(totalHeaders));
2372void MainWindow::onDependenciesError(
const QString &errorMessage) {
2373 QMessageBox::critical(
this, tr(
"Error"), errorMessage);
2374 logStatus(LogLevel::Error, tr(
"Error loading dependencies"),
2375 QStringLiteral(
"query-dependencies"));
2378void MainWindow::onDependenciesProgress(
const QString &message) {
2379 logStatus(LogLevel::Info, message, QStringLiteral(
"query-dependencies"));
2382void MainWindow::onAstProgress(
const QString &message) {
2383 logStatus(LogLevel::Info, message, QStringLiteral(
"ast-extractor"));
2386void MainWindow::onAstStatsUpdated(
const AstExtractionStats &stats) {
2387 logStatus(LogLevel::Info,
2388 QString(
"Comments found: %1").arg(stats.commentCount),
2389 QStringLiteral(
"ast-extractor"));
2392void MainWindow::onDependenciesReadyWithErrors(
2393 const QJsonObject &dependencies,
const QStringList &errorMessages) {
2395 tuModel_->populateFromDependencies(dependencies, projectRoot_,
2396 compilationDatabasePath_);
2397 expandFileExplorerTopLevel();
2399 QJsonObject stats = dependencies[
"statistics"].toObject();
2400 int successCount = stats[
"successCount"].toInt();
2401 int failureCount = stats[
"failureCount"].toInt();
2402 int totalHeaders = stats[
"totalHeaderCount"].toInt();
2404 logStatus(LogLevel::Warning,
2405 QString(
"Loaded %1 translation units (%2 failed)")
2408 QStringLiteral(
"query-dependencies"));
2409 for (
const QString &errorMessage : errorMessages) {
2410 logStatus(LogLevel::Error, errorMessage,
2411 QStringLiteral(
"query-dependencies"));
2414 QString(
"Dependencies summary: %1 sources loaded, %2 failed, %3 headers")
2417 .arg(totalHeaders));
2420void MainWindow::logStatus(LogLevel level,
const QString &message,
2421 const QString &source) {
2422 const QString trimmed = message.trimmed();
2423 if (trimmed.isEmpty()) {
2428 entry.level = level;
2429 entry.source = source.isEmpty() ? QStringLiteral(
"acav") : source;
2430 entry.message = trimmed;
2431 entry.timestamp = QDateTime::currentDateTime();
2434 QMetaObject::invokeMethod(logDock_,
"enqueue", Qt::QueuedConnection,
2435 Q_ARG(LogEntry, entry));
2439void MainWindow::onAstReady(
const QString &astFilePath) {
2440 MemoryProfiler::checkpoint(
"After make-ast generation complete");
2441 persistAstCompilationErrorState(astFilePath, astHasCompilationErrors_);
2443 extractAst(astFilePath, currentSourceFilePath_);
2446void MainWindow::onAstExtracted(AstViewNode *root) {
2447 MemoryProfiler::checkpoint(
"AST extraction complete - before rendering");
2450 clearAstSearchState();
2451 if (astSearchInput_) {
2452 astSearchInput_->clear();
2456 isAstExtractionInProgress_ =
false;
2460 if (!currentSourceFilePath_.isEmpty()) {
2462 if (sourceView_->currentFilePath() != currentSourceFilePath_) {
2464 if (sourceView_->loadFile(currentSourceFilePath_)) {
2467 fileManager_.registerFile(currentSourceFilePath_.toStdString());
2468 sourceView_->setCurrentFileId(fileId);
2469 updateSourceSubtitle(currentSourceFilePath_);
2472 sourceView_->setCurrentFileId(FileManager::InvalidFileID);
2473 logStatus(LogLevel::Warning, QString(
"Failed to reload source file: %1")
2474 .arg(currentSourceFilePath_));
2482 FileID currentFileId = sourceView_->currentFileId();
2483 if (currentFileId != FileManager::InvalidFileID) {
2484 QModelIndex tuIndex = tuModel_->findIndexByFileId(currentFileId);
2485 if (tuIndex.isValid()) {
2486 tuView_->setCurrentIndex(tuIndex);
2487 tuView_->scrollTo(tuIndex);
2492 auto renderStart = std::chrono::steady_clock::now();
2494 MemoryProfiler::checkpoint(
"Before setting root node to model");
2496 std::size_t nodeCount = 0;
2498 nodeCount = astContext_->getAstViewNodeCount();
2499 astModel_->setTotalNodeCount(nodeCount);
2503 astModel_->setRootNode(root);
2504 updateAstSubtitle(currentSourceFilePath_);
2506 MemoryProfiler::checkpoint(
"After setting root node to model");
2508 auto renderEnd = std::chrono::steady_clock::now();
2509 std::chrono::duration<double> renderElapsed = renderEnd - renderStart;
2510 onTimingMessage(QString(
"render AST: %1s")
2511 .arg(QString::number(renderElapsed.count(),
'f', 2)));
2512 onTimingMessage(QString(
"AST nodes loaded: %1").arg(nodeCount));
2514 MemoryProfiler::checkpoint(
2515 QString(
"AST rendering complete (%1 nodes)").arg(nodeCount));
2517 const bool showCompilationWarning = astHasCompilationErrors_;
2518 setAstCompilationWarningVisible(showCompilationWarning);
2519 astHasCompilationErrors_ =
false;
2521 logStatus(LogLevel::Info, tr(
"AST loaded (%1 nodes)").arg(nodeCount));
2524void MainWindow::onAstError(
const QString &errorMessage) {
2526 isAstExtractionInProgress_ =
false;
2527 astHasCompilationErrors_ =
false;
2528 setAstCompilationWarningVisible(
false);
2530 QString targetAstPath = lastAstFilePath_;
2531 if (targetAstPath.isEmpty() && !compilationDatabasePath_.isEmpty() &&
2532 !currentSourceFilePath_.isEmpty()) {
2533 targetAstPath = AppConfig::instance().getAstFilePath(
2534 compilationDatabasePath_, currentSourceFilePath_);
2535 lastAstFilePath_ = targetAstPath;
2538 if (targetAstPath.isEmpty() || currentSourceFilePath_.isEmpty()) {
2539 logStatus(LogLevel::Error,
"Error: " + errorMessage);
2540 QMessageBox::critical(
this,
"AST Error",
2541 "Failed to generate or load AST:\n\n" + errorMessage);
2545 const bool cacheLoadFailed =
2546 errorMessage.contains(
"Failed to load AST from file",
2547 Qt::CaseInsensitive) ||
2548 errorMessage.contains(
"Failed to load AST", Qt::CaseInsensitive);
2549 if (!cacheLoadFailed) {
2550 logStatus(LogLevel::Error,
"Error: " + errorMessage);
2551 QMessageBox::critical(
this,
"AST Error",
2552 "Failed to generate or load AST:\n\n" + errorMessage);
2556 logStatus(LogLevel::Warning,
2557 tr(
"Cached AST load failed; regenerating automatically."));
2558 logStatus(LogLevel::Info, errorMessage);
2560 if (!deleteCachedAst(targetAstPath)) {
2561 logStatus(LogLevel::Error,
2562 tr(
"Failed to delete cached AST file: %1").arg(targetAstPath));
2563 QMessageBox::critical(
this,
"AST Error",
2564 "Failed to delete cached AST file:\n\n" +
2569 logStatus(LogLevel::Warning, tr(
"Regenerating AST after load failure..."));
2570 astHasCompilationErrors_ =
false;
2571 makeAstRunner_->run(compilationDatabasePath_, currentSourceFilePath_,
2575void MainWindow::onMakeAstLogMessage(
const LogEntry &entry) {
2576 if (entry.source == QStringLiteral(
"make-ast") &&
2577 entry.level == LogLevel::Error) {
2578 astHasCompilationErrors_ =
true;
2582void MainWindow::onAstContextMenuRequested(
const QPoint &pos) {
2583 QModelIndex index = astView_->indexAt(pos);
2584 if (!index.isValid()) {
2588 auto *node =
static_cast<AstViewNode *
>(index.internalPointer());
2594 const bool isTranslationUnit = (node->getParent() ==
nullptr);
2596 ::QMenu menu(astView_);
2598 SourceRange macroRange(SourceLocation(FileManager::InvalidFileID, 0, 0),
2599 SourceLocation(FileManager::InvalidFileID, 0, 0));
2600 const bool hasMacroRange = getMacroSpellingRange(node, ¯oRange);
2601 if (hasMacroRange) {
2602 QAction *macroAction = menu.addAction(tr(
"Go to Macro Definition"));
2603 connect(macroAction, &QAction::triggered,
this, [
this, node, macroRange]() {
2604 navigateToRange(macroRange, node,
false);
2606 menu.addSeparator();
2610 QAction *expandAllAction = menu.addAction(tr(
"Expand All"));
2611 QAction *collapseAllAction = menu.addAction(tr(
"Collapse All"));
2612 connect(expandAllAction, &QAction::triggered,
this,
2613 &MainWindow::onExpandAllAstChildren);
2614 connect(collapseAllAction, &QAction::triggered,
this,
2615 &MainWindow::onCollapseAllAstChildren);
2618 QAction *viewDetailsAction = menu.addAction(tr(
"View Details..."));
2619 viewDetailsAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_I));
2620 connect(viewDetailsAction, &QAction::triggered,
this,
2621 [
this, node]() { onViewNodeDetails(node); });
2624 menu.addSeparator();
2625 QAction *exportAction = menu.addAction(tr(
"Export Subtree to JSON..."));
2626 exportAction->setEnabled(!isTranslationUnit && astModel_ &&
2627 astModel_->hasNodes());
2628 connect(exportAction, &QAction::triggered,
this,
2629 [
this, node]() { onExportAst(node); });
2631 menu.exec(astView_->viewport()->mapToGlobal(pos));
2634void MainWindow::onViewNodeDetails(AstViewNode *node) {
2639 const AcavJson &props = node->getProperties();
2640 QString kind = tr(
"Node");
2643 if (props.contains(
"kind") && props.at(
"kind").is_string()) {
2644 kind = QString::fromStdString(props.at(
"kind").get<InternedString>().str());
2646 if (props.contains(
"name") && props.at(
"name").is_string()) {
2647 name = QString::fromStdString(props.at(
"name").get<InternedString>().str());
2650 QString title = tr(
"Node Details - %1").arg(kind);
2651 if (!name.isEmpty()) {
2652 title += QStringLiteral(
" '%1'").arg(name);
2658 const SourceRange &range = node->getSourceRange();
2659 AcavJson sourceRangeJson = AcavJson::object();
2661 auto locationToJson = [
this](
const SourceLocation &loc) {
2663 obj[
"fileId"] =
static_cast<uint64_t
>(loc.fileID());
2664 obj[
"line"] =
static_cast<uint64_t
>(loc.line());
2665 obj[
"column"] =
static_cast<uint64_t
>(loc.column());
2666 std::string_view filePath = fileManager_.getFilePath(loc.fileID());
2667 if (!filePath.empty()) {
2668 obj[
"filePath"] = InternedString(std::string(filePath));
2673 sourceRangeJson[
"begin"] = locationToJson(range.begin());
2674 sourceRangeJson[
"end"] = locationToJson(range.end());
2675 propertiesCopy[
"sourceRange"] = std::move(sourceRangeJson);
2677 auto *dialog =
new NodeDetailsDialog(std::move(propertiesCopy), title,
this);
2678 dialog->setAttribute(Qt::WA_DeleteOnClose);
2681 dialog->activateWindow();
2684void MainWindow::onExportAst(AstViewNode *node) {
2688 if (isAstExtractionInProgress_) {
2689 logStatus(LogLevel::Info, tr(
"AST extraction in progress, please wait..."));
2692 if (isAstExportInProgress_) {
2693 logStatus(LogLevel::Info, tr(
"AST export already in progress..."));
2698 if (!currentSourceFilePath_.isEmpty()) {
2699 QFileInfo info(currentSourceFilePath_);
2700 defaultDir = info.absolutePath();
2702 defaultDir = QDir::homePath();
2706 defaultDir + QDir::separator() + buildDefaultExportFileName(node);
2708 QString targetPath =
2709 QFileDialog::getSaveFileName(
this, tr(
"Export AST Subtree"), suggested,
2710 tr(
"JSON Files (*.json);;All Files (*)"));
2711 if (targetPath.isEmpty()) {
2715 auto confirm = QMessageBox::question(
2716 this, tr(
"Export AST"),
2717 tr(
"Exporting this subtree can take some time.\n\nDo you want to "
2719 QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
2720 if (confirm != QMessageBox::Yes) {
2724 auto *progress =
new QProgressDialog(
this);
2725 progress->setAttribute(Qt::WA_DeleteOnClose);
2726 progress->setWindowTitle(tr(
"Exporting AST"));
2727 progress->setLabelText(tr(
"Exporting AST subtree to JSON in the background.\n"
2728 "You can close this window and continue working.\n"
2729 "You will be notified when the export finishes."));
2730 progress->setCancelButtonText(tr(
"Close"));
2731 progress->setRange(0, 0);
2732 progress->setWindowModality(Qt::NonModal);
2734 connect(progress, &QProgressDialog::canceled, progress,
2735 &QProgressDialog::close);
2737 isAstExportInProgress_ =
true;
2738 QPointer<MainWindow> self(
this);
2739 QPointer<QProgressDialog> progressPtr(progress);
2740 FileManager *fileManager = &fileManager_;
2741 AstViewNode *exportRoot = node;
2742 QString exportPath = targetPath;
2744 auto *thread =
new QThread(
this);
2745 astExportThread_ = thread;
2746 auto *worker =
new QObject();
2747 worker->moveToThread(thread);
2750 thread, &QThread::started, worker,
2751 [self, progressPtr, exportRoot, exportPath, fileManager, thread]() {
2752 QString errorMessage;
2754 AcavJson json = buildAstJsonTree(exportRoot, *fileManager);
2755 InternedString serialized = json.dump(2);
2756 const std::string &data = serialized.str();
2758 QFile outFile(exportPath);
2759 if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
2760 errorMessage = QObject::tr(
"Unable to open file for writing:\n%1")
2762 }
else if (outFile.write(data.c_str(),
2763 static_cast<qsizetype
>(data.size())) == -1) {
2764 errorMessage = QObject::tr(
"Failed to write data to file:\n%1")
2769 }
catch (
const std::exception &ex) {
2770 errorMessage = QObject::tr(
"Failed to serialize AST subtree:\n%1")
2775 QMetaObject::invokeMethod(
2777 [self, progressPtr, exportPath, errorMessage]() {
2781 self->isAstExportInProgress_ =
false;
2783 progressPtr->close();
2785 auto *notice =
new QMessageBox(self);
2786 notice->setAttribute(Qt::WA_DeleteOnClose);
2787 notice->setStandardButtons(QMessageBox::Ok);
2788 notice->setWindowModality(Qt::NonModal);
2789 if (!errorMessage.isEmpty()) {
2790 notice->setIcon(QMessageBox::Warning);
2791 notice->setWindowTitle(QObject::tr(
"Export Failed"));
2792 notice->setText(errorMessage);
2794 notice->setIcon(QMessageBox::Information);
2795 notice->setWindowTitle(QObject::tr(
"Export Complete"));
2796 notice->setText(QObject::tr(
"Exported AST subtree to:\n%1")
2798 self->logStatus(LogLevel::Info,
2799 QObject::tr(
"Exported AST subtree to %1")
2804 Qt::QueuedConnection);
2810 connect(thread, &QThread::finished, worker, &QObject::deleteLater);
2811 connect(thread, &QThread::finished, thread, &QObject::deleteLater);
2812 connect(thread, &QThread::finished,
this,
2813 [
this]() { astExportThread_ =
nullptr; });
2817QString MainWindow::buildDefaultExportFileName(AstViewNode *node)
const {
2819 return QStringLiteral(
"ast_subtree.json");
2822 const AcavJson &props = node->getProperties();
2823 auto getStr = [&](
const char *key) -> QString {
2824 if (props.contains(key) && props.at(key).is_string()) {
2825 return QString::fromStdString(props.at(key).get<InternedString>().str());
2830 QString kind = getStr(
"kind");
2834 for (
const char *key : {
"name",
"declName",
"memberName"}) {
2836 if (!name.isEmpty()) {
2841 QString base = name.isEmpty() ? kind : QString(
"%1_%2").arg(kind, name);
2842 if (base.isEmpty()) {
2843 base = QStringLiteral(
"ast_subtree");
2848 sanitized.reserve(base.size());
2849 for (QChar ch : base) {
2850 if (ch.isLetterOrNumber() || ch ==
'_' || ch ==
'-') {
2851 sanitized.append(ch);
2852 }
else if (!sanitized.endsWith(
'_')) {
2853 sanitized.append(
'_');
2857 if (sanitized.isEmpty()) {
2858 return QStringLiteral(
"ast_subtree.json");
2861 return sanitized +
".json";
2864void MainWindow::extractAst(
const QString &astFilePath,
2865 const QString &sourceFilePath) {
2866 MemoryProfiler::checkpoint(
"Starting extractAst - queuing to worker");
2869 QStringList fileList = getFileListForSource(sourceFilePath);
2870 lastAstFilePath_ = astFilePath;
2871 currentSourceFilePath_ = sourceFilePath;
2875 QString compDbPath = compilationDatabasePath_;
2876 QMetaObject::invokeMethod(
2877 astExtractorRunner_,
2878 [
this, astFilePath, fileList, compDbPath]() {
2879 astExtractorRunner_->run(astFilePath, fileList, compDbPath);
2881 Qt::QueuedConnection);
2885MainWindow::getFileListForSource(
const QString &sourceFilePath)
const {
2886 QStringList fileList;
2887 fileList.append(sourceFilePath);
2890 QStringList headers = tuModel_->getIncludedHeadersForSource(sourceFilePath);
2891 fileList.append(headers);
2896bool MainWindow::isSourceFile(
const QString &filePath)
const {
2897 static const QStringList sourceExtensions = {
2898 ".cpp",
".cc",
".cxx",
".c",
2901 for (
const QString &ext : sourceExtensions) {
2902 if (filePath.endsWith(ext, Qt::CaseInsensitive)) {
2909bool MainWindow::validateSourceLookup(FileID fileId) {
2910 if (isAstExtractionInProgress_) {
2911 QFileInfo currentInfo(pendingSourceFilePath_);
2912 logStatus(LogLevel::Info,
2913 tr(
"AST extraction in progress for %1. Please wait...")
2914 .arg(currentInfo.fileName()));
2917 if (!astModel_ || !astModel_->hasNodes()) {
2918 logStatus(LogLevel::Info,
2919 tr(
"No AST available (code not yet compiled). Double-click the "
2920 "file in File Explorer to generate an AST."));
2923 if (!currentSourceFilePath_.isEmpty() && sourceView_ &&
2924 isSourceFile(sourceView_->currentFilePath())) {
2925 FileID currentAstFileId = FileManager::InvalidFileID;
2927 fileManager_.tryGetFileId(currentSourceFilePath_.toStdString())) {
2928 currentAstFileId = *existing;
2930 if (currentAstFileId != FileManager::InvalidFileID &&
2931 currentAstFileId != fileId) {
2932 logStatus(LogLevel::Info,
2933 tr(
"No AST available for this file yet. Double-click it in "
2934 "File Explorer to generate an AST."));
2941void MainWindow::logNoNodeFound(FileID fileId,
const QString &fallbackMessage) {
2942 const auto &index = astContext_->getLocationIndex();
2943 if (!index.hasFile(fileId)) {
2944 logStatus(LogLevel::Info,
2945 tr(
"No AST data for this file in the current translation unit. "
2946 "If this is a header, it may not be included. Double-click a "
2947 "source file in File Explorer to load its AST."));
2949 logStatus(LogLevel::Info, fallbackMessage);
2953bool MainWindow::deleteCachedAst(
const QString &astFilePath) {
2954 QFile astFile(astFilePath);
2955 if (astFile.exists() && !astFile.remove()) {
2959 const QString statusPath = astCacheStatusFilePath(astFilePath);
2960 QFile statusFile(statusPath);
2961 if (statusFile.exists() && !statusFile.remove()) {
2962 logStatus(LogLevel::Warning,
2963 tr(
"Failed to delete AST cache status file: %1").arg(statusPath));
2968QString MainWindow::astCacheStatusFilePath(
const QString &astFilePath)
const {
2969 return astFilePath +
".status";
2972void MainWindow::persistAstCompilationErrorState(
const QString &astFilePath,
2973 bool hasCompilationErrors) {
2974 if (astFilePath.isEmpty()) {
2978 const QString statusPath = astCacheStatusFilePath(astFilePath);
2979 QFile statusFile(statusPath);
2980 if (!statusFile.open(QIODevice::WriteOnly | QIODevice::Truncate |
2982 logStatus(LogLevel::Warning,
2983 tr(
"Failed to write AST cache status file: %1").arg(statusPath));
2987 const QByteArray payload = hasCompilationErrors ? QByteArrayLiteral(
"1\n")
2988 : QByteArrayLiteral(
"0\n");
2989 if (statusFile.write(payload) != payload.size()) {
2990 logStatus(LogLevel::Warning,
2991 tr(
"Failed to persist AST cache status: %1").arg(statusPath));
2995bool MainWindow::loadAstCompilationErrorState(
const QString &astFilePath)
const {
2996 if (astFilePath.isEmpty()) {
3000 QFile statusFile(astCacheStatusFilePath(astFilePath));
3001 if (!statusFile.exists() ||
3002 !statusFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
3006 const QByteArray value = statusFile.readAll().trimmed().toLower();
3007 return value ==
"1" || value ==
"true";
3010void MainWindow::clearHistory() {
3016void MainWindow::recordHistory(FileID fileId,
unsigned line,
unsigned column,
3017 AstViewNode *node) {
3018 if (suppressHistory_ || fileId == FileManager::InvalidFileID) {
3022 NavEntry entry{fileId, line, column, node, astVersion_};
3023 if (!history_.empty()) {
3024 const NavEntry ¤t = history_[historyCursor_];
3025 if (current.fileId == entry.fileId && current.line == entry.line &&
3026 current.column == entry.column && current.node == entry.node &&
3027 current.astVersion == entry.astVersion) {
3033 if (historyCursor_ + 1 < history_.size()) {
3034 history_.erase(history_.begin() +
static_cast<long>(historyCursor_) + 1,
3038 history_.push_back(entry);
3039 historyCursor_ = history_.size() - 1;
3041 static constexpr std::size_t kMaxHistory = 500;
3042 if (history_.size() > kMaxHistory) {
3043 std::size_t toRemove = history_.size() - kMaxHistory;
3044 history_.erase(history_.begin(),
3045 history_.begin() +
static_cast<long>(toRemove));
3046 historyCursor_ = history_.size() - 1;
3052void MainWindow::navigateHistory(
int delta) {
3053 if (history_.empty()) {
3056 int newIndex =
static_cast<int>(historyCursor_) + delta;
3057 if (newIndex < 0 || newIndex >=
static_cast<int>(history_.size())) {
3060 historyCursor_ =
static_cast<std::size_t
>(newIndex);
3061 applyEntry(history_[historyCursor_]);
3065void MainWindow::applyEntry(
const NavEntry &entry) {
3066 if (entry.fileId == FileManager::InvalidFileID) {
3070 std::string_view path = fileManager_.getFilePath(entry.fileId);
3072 logStatus(LogLevel::Warning, tr(
"History target file is unavailable"));
3076 suppressHistory_ =
true;
3077 QString qPath = QString::fromStdString(std::string(path));
3079 if (sourceView_->currentFileId() != entry.fileId) {
3080 if (!sourceView_->loadFile(qPath)) {
3081 logStatus(LogLevel::Error, QString(
"Failed to load %1").arg(qPath));
3082 suppressHistory_ =
false;
3085 sourceView_->setCurrentFileId(entry.fileId);
3086 updateSourceSubtitle(qPath);
3089 highlightTuFile(entry.fileId);
3091 SourceLocation loc(entry.fileId, entry.line, entry.column);
3092 SourceRange range(loc, loc);
3093 sourceView_->highlightRange(range);
3095 if (entry.astVersion == astVersion_ && entry.node) {
3096 QModelIndex modelIndex = astModel_->selectNode(entry.node);
3097 if (modelIndex.isValid()) {
3098 astView_->selectionModel()->setCurrentIndex(
3099 modelIndex, QItemSelectionModel::ClearAndSelect);
3100 astView_->scrollTo(modelIndex);
3104 suppressHistory_ =
false;
3107void MainWindow::updateNavActions() {
3108 bool hasHistory = !history_.empty();
3109 if (navBackAction_) {
3110 navBackAction_->setEnabled(hasHistory && historyCursor_ > 0);
3111 if (hasHistory && historyCursor_ > 0) {
3112 const NavEntry &target = history_[historyCursor_ - 1];
3113 std::string_view path = fileManager_.getFilePath(target.fileId);
3114 navBackAction_->setToolTip(
3115 QString(
"Back to %1:%2")
3116 .arg(QString::fromStdString(std::string(path)))
3119 navBackAction_->setToolTip(tr(
"Back"));
3122 if (navForwardAction_) {
3123 navForwardAction_->setEnabled(hasHistory &&
3124 (historyCursor_ + 1 < history_.size()));
3125 if (hasHistory && historyCursor_ + 1 < history_.size()) {
3126 const NavEntry &target = history_[historyCursor_ + 1];
3127 std::string_view path = fileManager_.getFilePath(target.fileId);
3128 navForwardAction_->setToolTip(
3129 QString(
"Forward to %1:%2")
3130 .arg(QString::fromStdString(std::string(path)))
3133 navForwardAction_->setToolTip(tr(
"Forward"));
3138void MainWindow::onTimingMessage(
const QString &message) {
3139 QString trimmed = message.trimmed();
3140 if (trimmed.isEmpty()) {
3144 if (trimmed.startsWith(QStringLiteral(
"Timing "))) {
3147 logStatus(LogLevel::Debug, trimmed, QStringLiteral(
"acav-timing"));
3152void MainWindow::onAstNodeSelected(
const QModelIndex &index) {
3153 if (!index.isValid() || !astContext_) {
3157 AstViewNode *node =
static_cast<AstViewNode *
>(index.internalPointer());
3162 if (suppressSourceHighlight_) {
3163 suppressSourceHighlight_ =
false;
3164 astModel_->updateSelectionFromIndex(index);
3169 astModel_->updateSelectionFromIndex(index);
3171 const SourceRange &range = node->getSourceRange();
3172 if (goToMacroDefinitionAction_) {
3173 SourceRange macroRange(SourceLocation(FileManager::InvalidFileID, 0, 0),
3174 SourceLocation(FileManager::InvalidFileID, 0, 0));
3175 bool hasMacroRange = getMacroSpellingRange(node, ¯oRange);
3176 goToMacroDefinitionAction_->setEnabled(hasMacroRange);
3178 bool skipCursorMove = suppressSourceCursorMove_;
3179 suppressSourceCursorMove_ =
false;
3180 navigateToRange(range, node, skipCursorMove);
3183bool MainWindow::getMacroSpellingRange(
const AstViewNode *node,
3184 SourceRange *outRange)
const {
3185 if (!node || !outRange) {
3188 const auto &properties = node->getProperties();
3189 auto it = properties.find(
"macroSpellingRange");
3190 if (it == properties.end()) {
3193 return parseSourceRangeJson(*it, outRange);
3196bool MainWindow::navigateToRange(
const SourceRange &range, AstViewNode *node,
3197 bool skipCursorMove) {
3198 FileID rangeFileId = range.begin().fileID();
3201 if (rangeFileId == FileManager::InvalidFileID) {
3204 if (!currentSourceFilePath_.isEmpty()) {
3206 fileManager_.tryGetFileId(currentSourceFilePath_.toStdString())) {
3207 highlightTuFile(*fileId);
3211 sourceView_->clearHighlight();
3216 if (rangeFileId != sourceView_->currentFileId()) {
3218 std::string_view filePath = fileManager_.getFilePath(rangeFileId);
3219 if (filePath.empty()) {
3220 logStatus(LogLevel::Warning, tr(
"Cannot find file path for AST node"));
3225 QString qFilePath = QString::fromStdString(std::string(filePath));
3226 if (!sourceView_->loadFile(qFilePath)) {
3227 logStatus(LogLevel::Error,
3228 QString(
"Failed to load file: %1").arg(qFilePath));
3233 sourceView_->setCurrentFileId(rangeFileId);
3234 updateSourceSubtitle(qFilePath);
3238 sourceView_->highlightRange(range, !skipCursorMove);
3239 highlightTuFile(rangeFileId);
3242 recordHistory(rangeFileId, range.begin().line(), range.begin().column(),
3249void MainWindow::onGoToMacroDefinition() {
3250 if (!astContext_ || !astView_) {
3253 QModelIndex index = astView_->currentIndex();
3254 if (!index.isValid()) {
3257 AstViewNode *node =
static_cast<AstViewNode *
>(index.internalPointer());
3261 SourceRange macroRange(SourceLocation(FileManager::InvalidFileID, 0, 0),
3262 SourceLocation(FileManager::InvalidFileID, 0, 0));
3263 if (!getMacroSpellingRange(node, ¯oRange)) {
3264 logStatus(LogLevel::Info, tr(
"No macro definition for this AST node"));
3267 navigateToRange(macroRange, node,
false);
3270void MainWindow::onSourcePositionClicked(FileID fileId,
unsigned line,
3272 if (!astContext_ || fileId == FileManager::InvalidFileID) {
3275 if (!validateSourceLookup(fileId)) {
3279 const auto &index = astContext_->getLocationIndex();
3280 auto matches = index.getNodesAt(fileId, line, column);
3282 if (matches.empty()) {
3283 logNoNodeFound(fileId, tr(
"No AST node at this position"));
3294 auto getRangeSize = [](
const SourceRange &range) -> RangeSize {
3295 unsigned lines = range.end().line() - range.begin().line();
3296 unsigned columns = 0;
3299 columns = range.end().column() - range.begin().column();
3304 return {lines, columns};
3307 auto compareRangeSize = [](
const RangeSize &a,
const RangeSize &b) ->
int {
3308 if (a.lines != b.lines)
3309 return a.lines < b.lines ? -1 : 1;
3310 if (a.columns != b.columns)
3311 return a.columns < b.columns ? -1 : 1;
3316 RangeSize minSize = getRangeSize(matches[0]->getSourceRange());
3317 for (
auto *node : matches) {
3318 RangeSize size = getRangeSize(node->getSourceRange());
3319 if (compareRangeSize(size, minSize) < 0) {
3325 std::vector<AstViewNode *> mostSpecific;
3326 for (
auto *node : matches) {
3327 RangeSize size = getRangeSize(node->getSourceRange());
3328 if (compareRangeSize(size, minSize) == 0) {
3329 mostSpecific.push_back(node);
3334 AstViewNode *selected = mostSpecific.front();
3336 QModelIndex modelIndex = astModel_->selectNode(selected);
3337 if (modelIndex.isValid()) {
3338 suppressSourceCursorMove_ =
true;
3339 astView_->selectionModel()->setCurrentIndex(
3340 modelIndex, QItemSelectionModel::ClearAndSelect);
3341 astView_->scrollTo(modelIndex);
3342 suppressSourceCursorMove_ =
false;
3343 const SourceRange &range = selected->getSourceRange();
3344 recordHistory(range.begin().fileID(), range.begin().line(),
3345 range.begin().column(), selected);
3349void MainWindow::onSourceRangeSelected(FileID fileId,
unsigned startLine,
3350 unsigned startColumn,
unsigned endLine,
3351 unsigned endColumn) {
3352 if (!astContext_ || fileId == FileManager::InvalidFileID) {
3355 if (!validateSourceLookup(fileId)) {
3359 const auto &index = astContext_->getLocationIndex();
3360 AstViewNode *match = index.getFirstNodeContainedInRange(
3361 fileId, startLine, startColumn, endLine, endColumn);
3364 logNoNodeFound(fileId, tr(
"No AST node within selection"));
3368 QModelIndex modelIndex = astModel_->selectNode(match);
3369 if (modelIndex.isValid()) {
3370 suppressSourceHighlight_ =
true;
3371 astView_->selectionModel()->setCurrentIndex(
3372 modelIndex, QItemSelectionModel::ClearAndSelect);
3373 astView_->scrollTo(modelIndex);
3374 const SourceRange &range = match->getSourceRange();
3375 recordHistory(range.begin().fileID(), range.begin().line(),
3376 range.begin().column(), match);
3380void MainWindow::highlightTuFile(FileID fileId) {
3381 if (!tuModel_ || !tuView_) {
3385 QModelIndex tuIndex;
3387 if (fileId != FileManager::InvalidFileID) {
3388 tuIndex = tuModel_->findIndexByFileId(fileId);
3391 if (!tuIndex.isValid() && fileId != FileManager::InvalidFileID) {
3392 if (!currentSourceFilePath_.isEmpty()) {
3393 std::string_view path = fileManager_.getFilePath(fileId);
3394 if (!path.empty()) {
3395 QString qPath = QString::fromStdString(std::string(path));
3396 QModelIndex sourceRoot =
3397 tuModel_->findIndexByFilePath(currentSourceFilePath_);
3398 if (sourceRoot.isValid()) {
3399 if (tuModel_->canFetchMore(sourceRoot)) {
3400 tuModel_->fetchMore(sourceRoot);
3402 tuIndex = tuModel_->findIndexByAnyFilePathUnder(qPath, sourceRoot);
3408 if (!tuIndex.isValid()) {
3413 QModelIndex parent = tuIndex.parent();
3414 while (parent.isValid()) {
3415 tuView_->expand(parent);
3416 parent = parent.parent();
3420 tuView_->selectionModel()->setCurrentIndex(
3421 tuIndex, QItemSelectionModel::ClearAndSelect);
3422 tuView_->scrollTo(tuIndex, QAbstractItemView::PositionAtCenter);
3424 QRect rect = tuView_->visualRect(tuIndex);
3425 if (rect.isValid()) {
3427 QModelIndex p = tuIndex.parent();
3428 while (p.isValid()) {
3433 QString text = tuIndex.data(Qt::DisplayRole).toString();
3434 QFontMetrics fm(tuView_->font());
3435 int textWidth = fm.horizontalAdvance(text);
3438 QVariant iconVar = tuIndex.data(Qt::DecorationRole);
3439 if (iconVar.canConvert<QIcon>()) {
3440 QSize iconSize = tuView_->iconSize();
3441 if (iconSize.isValid()) {
3442 iconWidth = iconSize.width() + 4;
3446 int indent = tuView_->indentation() * depth;
3448 int textLeft = rect.left() + indent + iconWidth + padding;
3449 int textRight = textLeft + textWidth;
3451 QScrollBar *hBar = tuView_->horizontalScrollBar();
3452 int viewWidth = tuView_->viewport()->width();
3454 hBar->setValue(hBar->value() + textLeft);
3455 }
else if (textRight > viewWidth) {
3456 hBar->setValue(hBar->value() + (textRight - viewWidth));
3461void MainWindow::onCycleNodeSelected(AstViewNode *node) {
3466 QModelIndex modelIndex = astModel_->selectNode(node);
3467 if (modelIndex.isValid()) {
3468 astView_->selectionModel()->setCurrentIndex(
3469 modelIndex, QItemSelectionModel::ClearAndSelect);
3470 astView_->scrollTo(modelIndex);
3473 const SourceRange &range = node->getSourceRange();
3474 highlightTuFile(range.begin().fileID());
3475 if (range.begin().fileID() == sourceView_->currentFileId()) {
3476 sourceView_->highlightRange(range);
3478 recordHistory(range.begin().fileID(), range.begin().line(),
3479 range.begin().column(), node);
3483void MainWindow::onCycleWidgetClosed() {
3487void MainWindow::onExpandAllAstChildren() { expandAllChildren(astView_); }
3489void MainWindow::onCollapseAllAstChildren() { collapseAllChildren(astView_); }
3493bool isTuSourceNode(
const QModelIndex &index) {
3494 return index.data(Qt::UserRole + 3).toBool();
3498bool isSourceFileOrDescendant(
const QModelIndex &index) {
3499 QModelIndex current = index;
3500 while (current.isValid()) {
3501 if (isTuSourceNode(current)) {
3504 current = current.parent();
3511void MainWindow::onTuContextMenuRequested(
const QPoint &pos) {
3512 QModelIndex index = tuView_->indexAt(pos);
3513 if (!index.isValid()) {
3518 tuView_->setCurrentIndex(index);
3520 ::QMenu menu(tuView_);
3526 QAction *expandAction = menu.addAction(tr(
"Expand All"));
3527 QAction *collapseAction = menu.addAction(tr(
"Collapse All"));
3529 connect(expandAction, &QAction::triggered,
this,
3530 &MainWindow::onExpandAllTuChildren);
3531 connect(collapseAction, &QAction::triggered,
this,
3532 &MainWindow::onCollapseAllTuChildren);
3534 menu.exec(tuView_->viewport()->mapToGlobal(pos));
3537void MainWindow::onExpandAllTuChildren() {
3538 QModelIndex currentIndex = tuView_->currentIndex();
3539 if (!currentIndex.isValid()) {
3546 if (isSourceFileOrDescendant(currentIndex)) {
3547 expandSubtree(tuView_);
3549 expandAllChildren(tuView_);
3553void MainWindow::onCollapseAllTuChildren() {
3554 QModelIndex currentIndex = tuView_->currentIndex();
3555 if (!currentIndex.isValid()) {
3562 if (isSourceFileOrDescendant(currentIndex)) {
3563 collapseAllChildren(tuView_);
3565 collapseTuDirectories(tuView_);
3569void MainWindow::expandAllChildren(QTreeView *view) {
3570 QModelIndex currentIndex = view->currentIndex();
3571 if (!currentIndex.isValid()) {
3576 QHeaderView::ResizeMode prevResizeMode = view->header()->sectionResizeMode(0);
3577 view->header()->setSectionResizeMode(QHeaderView::Fixed);
3578 view->setUpdatesEnabled(
false);
3582 if (view->selectionModel()) {
3583 view->selectionModel()->blockSignals(
true);
3586 if (view == tuView_) {
3589 std::vector<QModelIndex> stack;
3591 stack.push_back(currentIndex);
3593 QAbstractItemModel *model = view->model();
3594 while (!stack.empty()) {
3595 QModelIndex idx = stack.back();
3597 if (!idx.isValid()) {
3601 if (isTuSourceNode(idx)) {
3605 int childCount = model->rowCount(idx);
3606 if (childCount <= 0) {
3610 if (!view->isExpanded(idx)) {
3614 for (
int row = 0; row < childCount; ++row) {
3615 stack.push_back(model->index(row, 0, idx));
3620 view->expandRecursively(currentIndex);
3623 if (view->selectionModel()) {
3624 view->selectionModel()->blockSignals(
false);
3626 view->setUpdatesEnabled(
true);
3627 view->header()->setSectionResizeMode(prevResizeMode);
3630void MainWindow::expandSubtree(QTreeView *view) {
3635 QModelIndex currentIndex = view->currentIndex();
3636 if (!currentIndex.isValid()) {
3640 QHeaderView::ResizeMode prevResizeMode = view->header()->sectionResizeMode(0);
3641 view->header()->setSectionResizeMode(QHeaderView::Fixed);
3642 view->setUpdatesEnabled(
false);
3643 if (view->selectionModel()) {
3644 view->selectionModel()->blockSignals(
true);
3647 QAbstractItemModel *model = view->model();
3648 std::vector<QModelIndex> stack;
3650 stack.push_back(currentIndex);
3652 while (!stack.empty()) {
3653 QModelIndex idx = stack.back();
3655 if (!idx.isValid()) {
3659 if (model->canFetchMore(idx)) {
3660 model->fetchMore(idx);
3663 int childCount = model->rowCount(idx);
3664 if (childCount > 0 && !view->isExpanded(idx)) {
3668 for (
int row = 0; row < childCount; ++row) {
3669 stack.push_back(model->index(row, 0, idx));
3673 if (view->selectionModel()) {
3674 view->selectionModel()->blockSignals(
false);
3676 view->setUpdatesEnabled(
true);
3677 view->header()->setSectionResizeMode(prevResizeMode);
3680void MainWindow::collapseAllChildren(QTreeView *view) {
3681 QModelIndex currentIndex = view->currentIndex();
3682 if (!currentIndex.isValid()) {
3686 QHeaderView::ResizeMode prevResizeMode = view->header()->sectionResizeMode(0);
3687 view->header()->setSectionResizeMode(QHeaderView::Fixed);
3688 view->setUpdatesEnabled(
false);
3692 if (view->selectionModel()) {
3693 view->selectionModel()->blockSignals(
true);
3696 if (!currentIndex.parent().isValid()) {
3699 view->collapseAll();
3702 collapseRecursively(currentIndex, view);
3705 if (view->selectionModel()) {
3706 view->selectionModel()->blockSignals(
false);
3708 view->setUpdatesEnabled(
true);
3709 view->header()->setSectionResizeMode(prevResizeMode);
3712void MainWindow::collapseTuDirectories(QTreeView *view) {
3717 QModelIndex currentIndex = view->currentIndex();
3718 if (!currentIndex.isValid()) {
3722 QHeaderView::ResizeMode prevResizeMode = view->header()->sectionResizeMode(0);
3723 view->header()->setSectionResizeMode(QHeaderView::Fixed);
3724 view->setUpdatesEnabled(
false);
3725 if (view->selectionModel()) {
3726 view->selectionModel()->blockSignals(
true);
3729 QAbstractItemModel *model = view->model();
3730 std::vector<QModelIndex> stack;
3731 std::vector<QModelIndex> postOrder;
3733 postOrder.reserve(256);
3734 stack.push_back(currentIndex);
3736 while (!stack.empty()) {
3737 QModelIndex idx = stack.back();
3739 if (!idx.isValid()) {
3743 postOrder.push_back(idx);
3745 int rowCount = model->rowCount(idx);
3746 for (
int row = 0; row < rowCount; ++row) {
3747 QModelIndex child = model->index(row, 0, idx);
3750 if (view->isExpanded(child)) {
3751 stack.push_back(child);
3756 for (std::size_t i = postOrder.size(); i-- > 0;) {
3757 view->collapse(postOrder[i]);
3760 if (view->selectionModel()) {
3761 view->selectionModel()->blockSignals(
false);
3764 view->setUpdatesEnabled(
true);
3765 view->header()->setSectionResizeMode(prevResizeMode);
3768void MainWindow::collapseRecursively(
const QModelIndex &index,
3770 if (!index.isValid() || !view) {
3774 QAbstractItemModel *model = view->model();
3775 std::vector<QModelIndex> stack;
3776 std::vector<QModelIndex> postOrder;
3778 postOrder.reserve(256);
3779 stack.push_back(index);
3781 while (!stack.empty()) {
3782 QModelIndex idx = stack.back();
3784 if (!idx.isValid()) {
3788 postOrder.push_back(idx);
3790 int rowCount = model->rowCount(idx);
3791 for (
int row = 0; row < rowCount; ++row) {
3792 QModelIndex child = model->index(row, 0, idx);
3793 if (view->isExpanded(child)) {
3794 stack.push_back(child);
3799 for (std::size_t i = postOrder.size(); i-- > 0;) {
3800 view->collapse(postOrder[i]);
3804void MainWindow::updateSourceSubtitle(
const QString &filePath) {
3805 if (!sourceTitleBar_ || filePath.isEmpty()) {
3806 if (sourceTitleBar_) {
3807 sourceTitleBar_->setSubtitle(QString());
3812 QString projectRoot = tuModel_ ? tuModel_->projectRoot() : QString();
3815 if (!projectRoot.isEmpty() && filePath.startsWith(projectRoot +
"/")) {
3817 QString relativePath = filePath.mid(projectRoot.length() + 1);
3818 subtitle = QStringLiteral(
"[project] %1").arg(relativePath);
3821 subtitle = QStringLiteral(
"[external] %1").arg(filePath);
3824 sourceTitleBar_->setSubtitle(subtitle);
3827void MainWindow::updateAstSubtitle(
const QString &mainSourcePath) {
3828 if (!astTitleBar_ || mainSourcePath.isEmpty()) {
3830 astTitleBar_->setSubtitle(QString());
3835 QString projectRoot = tuModel_ ? tuModel_->projectRoot() : QString();
3838 if (!projectRoot.isEmpty() && mainSourcePath.startsWith(projectRoot +
"/")) {
3840 QString relativePath = mainSourcePath.mid(projectRoot.length() + 1);
3841 subtitle = QStringLiteral(
"[project] %1").arg(relativePath);
3844 subtitle = QStringLiteral(
"[external] %1").arg(mainSourcePath);
3847 astTitleBar_->setSubtitle(subtitle);
Application configuration and settings.
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.
Utilities for interacting with Clang at runtime This includes runtime detection of Clang paths and AS...
std::vector< std::string > getSourceFilesFromCompilationDatabase(const std::string &compDbPath, std::string &errorMessage)
Extract source file paths from a compilation database.
std::string getClangResourceDir(const std::string &overrideResourceDir="")
Get clang resource directory.
std::size_t FileID
Type-safe identifier for registered files. 0 is reserved for invalid.
Memory-efficient immutable string with automatic deduplication.
Main application window with multi-panel layout.
Cross-platform memory profiling utility.
Non-modal dialog displaying AST node properties in a tree view.
Dialog for opening a project with compilation database and optional project root.
Source code location representation.
Application configuration manager.
QString getDependenciesFilePath(const QString &compilationDatabasePath) const
Get the dependencies JSON file path for a compilation database.
QString getCacheDirectory(const QString &compilationDatabasePath) const
Get the cache directory for a compilation database.
static AppConfig & instance()
Get the singleton instance.
void loadCompilationDatabase(const QString &compilationDatabasePath, const QString &projectRoot=QString())
Load compilation database and populate translation units.
static void checkpoint(const QString &label)
Log current memory usage with a label.