ACAV f0ba4b7c9529
Abstract Syntax Tree (AST) visualization tool for C, C++, and Objective-C
Loading...
Searching...
No Matches
MainWindow.cpp
1/*$!{
2* Aurora Clang AST Viewer (ACAV)
3*
4* Copyright (c) 2026 Min Liu
5* Copyright (c) 2026 Michael David Adams
6*
7* SPDX-License-Identifier: GPL-2.0-or-later
8*
9* This program is free software; you can redistribute it and/or modify
10* it under the terms of the GNU General Public License as published by
11* the Free Software Foundation; either version 2 of the License, or
12* (at your option) any later version.
13*
14* This program is distributed in the hope that it will be useful,
15* but WITHOUT ANY WARRANTY; without even the implied warranty of
16* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17* GNU General Public License for more details.
18*
19* You should have received a copy of the GNU General Public License along
20* with this program; if not, see <https://www.gnu.org/licenses/>.
21}$!*/
22
23#include "ui/MainWindow.h"
24#include "Version.h"
25#include "common/ClangUtils.h"
27#include "core/AppConfig.h"
28#include "core/MemoryProfiler.h"
29#include "core/SourceLocation.h"
32#include <QAbstractItemView>
33#include <QAction>
34#include <QApplication>
35#include <QButtonGroup>
36#include <QColor>
37#include <QComboBox>
38#include <QCompleter>
39#include <QDateTime>
40#include <QDesktopServices>
41#include <QDialogButtonBox>
42#include <QDialog>
43#include <QDir>
44#include <QFile>
45#include <QFileDialog>
46#include <QFileInfo>
47#include <QFocusEvent>
48#include <QFontMetrics>
49#include <QFrame>
50#include <QGroupBox>
51#include <QHBoxLayout>
52#include <QHeaderView>
53#include <QIcon>
54#include <QKeySequence>
55#include <QListWidget>
56#include <QMenu>
57#include <QMenuBar>
58#include <QMessageBox>
59#include <QMetaObject>
60#include <QMetaType>
61#include <QMoveEvent>
62#include <QMouseEvent>
63#include <QPalette>
64#include <QPlainTextEdit>
65#include <QPointer>
66#include <QProgressDialog>
67#include <QPushButton>
68#include <QRadioButton>
69#include <QRegularExpression>
70#include <QScrollBar>
71#include <QShortcut>
72#include <QStringListModel>
73#include <QStyle>
74#include <QTabBar>
75#include <QTabWidget>
76#include <QTableWidget>
77#include <QTimer>
78#include <QToolBar>
79#include <QResizeEvent>
80#include <QUrl>
81#include <QVBoxLayout>
82#include <QWidget>
83#include <chrono>
84#include <cstddef>
85#include <exception>
86#include <unordered_set>
87#include <vector>
88
89namespace acav {
90
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) {
108
109 qRegisterMetaType<LogEntry>();
110
111 // Set window properties early so initial dock sizing is based on the final
112 // window geometry (otherwise QMainWindow/QSplitter will scale dock sizes).
113 setWindowTitle("ACAV");
114 setWindowIcon(QIcon(":/resources/aurora_icon.png"));
115 // Default window size
116 resize(1600, 900);
117
118 setupUI();
119 setupMenuBar();
120 setupDockWidgets();
121 setupModels();
122 connectSignals();
123
124 // Apply configured font size to all panels for consistent initial appearance
125 applyFontSize(AppConfig::instance().getFontSize());
126
127 logStatus(LogLevel::Info, tr("Ready"));
128}
129
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);
136 }
137 MemoryProfiler::setLogCallback(nullptr);
138 // Detach the AST model before destroying the AstContext to avoid
139 // views touching freed nodes during QWidget teardown.
140 if (astView_) {
141 astView_->setModel(nullptr);
142 }
143 if (astModel_) {
144 astModel_->clear();
145 }
146
147 // Clean shutdown of worker thread
148 if (astWorkerThread_) {
149 astWorkerThread_->quit();
150 astWorkerThread_->wait();
151 }
152 if (astExportThread_) {
153 astExportThread_->quit();
154 astExportThread_->wait();
155 }
156 // Qt parent-child ownership handles rest of cleanup
157}
158
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);
168}
169
170void MainWindow::setupViewMenuDockActions() {
171 if (!viewMenu_) {
172 return;
173 }
174
175 viewMenu_->addSeparator();
176
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) {
184 if (dock) {
185 QAction *action = dock->toggleViewAction();
186 action->setText(text);
187 viewMenu_->addAction(action);
188 }
189 }
190
191 viewMenu_->addSeparator();
192
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_);
198}
199
200void MainWindow::setupUI() {
201 applyUnifiedSelectionPalette();
202
203 setDockNestingEnabled(true);
204 setDockOptions(QMainWindow::AnimatedDocks | QMainWindow::AllowNestedDocks |
205 QMainWindow::AllowTabbedDocks);
206
207 // No central widget - use dock-only layout
208 setCentralWidget(nullptr);
209}
210
211void MainWindow::setupMenuBar() {
212 // Create File menu
213 fileMenu_ = menuBar()->addMenu(tr("&File"));
214
215 // Open Project action
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);
235
236 fileMenu_->addSeparator();
237
238 // Exit action
239 exitAction_ = new QAction(tr("E&xit"), this);
240 exitAction_->setShortcut(QKeySequence::Quit);
241 exitAction_->setStatusTip(tr("Exit the application"));
242 fileMenu_->addAction(exitAction_);
243
244 // View menu
245 viewMenu_ = menuBar()->addMenu(tr("&View"));
246
247 // Navigation actions (Cmd+[ / Cmd+] on macOS, Ctrl+[ / Ctrl+] elsewhere)
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); });
262
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(); });
272
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); });
281
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); });
289
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_);
301 }
302 baseFont.setPointSize(defaultSize);
303
304 // Reset only the focused panel's font size
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);
317 }
318 if (astSearchPopup_) {
319 astSearchPopup_->setFont(baseFont);
320 }
321 if (astSearchCompleter_ && astSearchCompleter_->popup()) {
322 astSearchCompleter_->popup()->setFont(baseFont);
323 }
324 } else if (focusedDock_ == declContextDock_ && declContextView_) {
325 declContextFontSize_ = defaultSize;
326 declContextView_->applyFont(baseFont);
327 } else if (focusedDock_ == logDock_) {
328 logFontSize_ = defaultSize;
329 logDock_->setFont(baseFont);
330 } else {
331 // Fallback: reset all if no dock focused
332 tuFontSize_ = defaultSize;
333 sourceFontSize_ = defaultSize;
334 astFontSize_ = defaultSize;
335 declContextFontSize_ = defaultSize;
336 logFontSize_ = defaultSize;
337 applyFontSize(defaultSize);
338 }
339 });
340
341 // Optional toolbar for navigation
342 navToolBar_ = addToolBar(tr("Navigation"));
343 navToolBar_->addAction(navBackAction_);
344 navToolBar_->addAction(navForwardAction_);
345 navToolBar_->setMovable(false);
346
347 // Help menu
348 ::QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
349
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);
356
357 QColor headerBg = palette().color(QPalette::Highlight);
358 QColor headerFg = palette().color(QPalette::HighlightedText);
359
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);
376 };
377
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();
383 f.setBold(true);
384 item->setFont(f);
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);
392 };
393
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();
400 f.setBold(true);
401 keyItem->setFont(f);
402 table->setItem(row, 0, keyItem);
403 table->setItem(row, 1, new QTableWidgetItem(action));
404 };
405
406 auto finishTable = [](QTableWidget *table) {
407 table->resizeColumnsToContents();
408 table->resizeRowsToContents();
409 table->setMinimumWidth(table->columnWidth(0) + table->columnWidth(1) +
410 30);
411 };
412
413 // Left column
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);
433
434 // Right column
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);
451
452 auto *columnsLayout = new QHBoxLayout;
453 columnsLayout->addWidget(leftGroup);
454 columnsLayout->addWidget(rightGroup);
455
456 auto *closeButton = new QPushButton(tr("Close"), dialog);
457 connect(closeButton, &QPushButton::clicked, dialog, &QDialog::close);
458
459 auto *buttonLayout = new QHBoxLayout;
460 buttonLayout->addStretch();
461 buttonLayout->addWidget(closeButton);
462
463 auto *mainLayout = new QVBoxLayout(dialog);
464 mainLayout->addLayout(columnsLayout);
465 mainLayout->addLayout(buttonLayout);
466
467 dialog->resize(680, 480);
468 dialog->show();
469 });
470 helpMenu->addAction(shortcutsAction);
471
472 helpMenu->addSeparator();
473
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);
478}
479
480void MainWindow::onOpenConfigFile() {
481 QString configPath = AppConfig::instance().getConfigFilePath();
482
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.")
489 .arg(configPath));
490 return;
491 }
492
493 // Open config file with system default editor
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")
500 .arg(configPath));
501 } else {
502 logStatus(LogLevel::Info,
503 tr("Opened config file: %1").arg(fileInfo.fileName()));
504 }
505}
506
507void MainWindow::onReloadConfig() {
508 AppConfig &config = AppConfig::instance();
509 const bool configAvailable = config.reload();
510
511 applyFontSize(config.getFontSize());
512
513 if (parallelQueryRunner_) {
514 parallelQueryRunner_->setParallelCount(config.getParallelProcessorCount());
515 }
516 if (astExtractorRunner_) {
517 astExtractorRunner_->setCommentExtractionEnabled(
518 config.getCommentExtractionEnabled());
519 }
520
521 if (!configAvailable) {
522 logStatus(LogLevel::Warning,
523 tr("Config file missing; created defaults and reloaded."));
524 return;
525 }
526
527 logStatus(LogLevel::Info,
528 tr("Reloaded configuration: %1").arg(config.getConfigFilePath()));
529}
530
531void MainWindow::onResetLayout() {
532 if (defaultDockState_.isEmpty()) {
533 return;
534 }
535 restoreState(defaultDockState_);
536}
537
538void MainWindow::onFocusChanged(QWidget *old, QWidget *now) {
539 Q_UNUSED(old);
540
541 // Map docks to their title bars for data-driven focus handling
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_}};
548
549 // Find which dock now has focus
550 QDockWidget *activeDock = nullptr;
551 if (now && astSearchPopup_ && astSearchPopup_->isAncestorOf(now)) {
552 activeDock = astDock_;
553 }
554 for (const auto &[dock, titleBar] : dockTitleBars) {
555 if (activeDock) {
556 break;
557 }
558 if (now && dock && dock->isAncestorOf(now)) {
559 activeDock = dock;
560 break;
561 }
562 }
563
564 if (focusedDock_ != activeDock) {
565 // Update title bars using fast QPalette (not setStyleSheet)
566 for (const auto &[dock, titleBar] : dockTitleBars) {
567 if (titleBar) {
568 titleBar->setFocused(dock == activeDock);
569 }
570 }
571 focusedDock_ = activeDock;
572 }
573}
574
575void MainWindow::resizeEvent(QResizeEvent *event) {
576 QMainWindow::resizeEvent(event);
577 if (astSearchPopup_ && astSearchPopup_->isVisible()) {
578 syncAstSearchPopupGeometry();
579 }
580}
581
582void MainWindow::moveEvent(QMoveEvent *event) {
583 QMainWindow::moveEvent(event);
584 if (astSearchPopup_ && astSearchPopup_->isVisible()) {
585 syncAstSearchPopupGeometry();
586 }
587}
588
589bool MainWindow::eventFilter(QObject *watched, QEvent *event) {
590 if (watched == astDock_ && astSearchPopup_ && astSearchPopup_->isVisible()) {
591 switch (event->type()) {
592 case QEvent::Resize:
593 case QEvent::Move:
594 case QEvent::Show:
595 case QEvent::Hide:
596 syncAstSearchPopupGeometry();
597 break;
598 default:
599 break;
600 }
601 }
602 return QMainWindow::eventFilter(watched, event);
603}
604
605void MainWindow::onShowAbout() {
606 QString aboutText =
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>"
612 "<hr>"
613 "<p>A tool for visualizing and exploring Clang Abstract Syntax Trees "
614 "(AST) "
615 "with support for C++20 modules.</p>"
616 "<p><b>Features:</b></p>"
617 "<ul>"
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>"
621 "</ul>"
622 "<p>Built with Qt and LLVM/Clang.</p>")
623 .arg(ACAV_VERSION_STRING);
624
625 QMessageBox::about(this, tr("About ACAV"), aboutText);
626}
627
628namespace {
629
630class HistoryLineEdit : public QLineEdit {
631public:
632 explicit HistoryLineEdit(QWidget *parent = nullptr) : QLineEdit(parent) {}
633
634protected:
635 void focusInEvent(QFocusEvent *event) override {
636 QLineEdit::focusInEvent(event);
637 showHistoryPopup();
638 }
639
640 void mousePressEvent(QMouseEvent *event) override {
641 QLineEdit::mousePressEvent(event);
642 if (event->button() == Qt::LeftButton) {
643 showHistoryPopup();
644 }
645 }
646
647private:
648 void showHistoryPopup() {
649 QCompleter *historyCompleter = completer();
650 if (!historyCompleter) {
651 return;
652 }
653 historyCompleter->setCompletionPrefix(text().trimmed());
654 historyCompleter->complete();
655 }
656};
657
658AcavJson sourceLocationToJson(const SourceLocation &loc,
659 const FileManager &fileManager) {
660 AcavJson obj = AcavJson::object();
661 if (!loc.isValid()) {
662 obj["valid"] = false;
663 return obj;
664 }
665
666 obj["valid"] = true;
667 obj["fileId"] = loc.fileID();
668 obj["line"] = loc.line();
669 obj["column"] = loc.column();
670
671 std::string_view filePath = fileManager.getFilePath(loc.fileID());
672 if (!filePath.empty()) {
673 obj["filePath"] = InternedString(std::string(filePath));
674 }
675
676 return obj;
677}
678
679AcavJson sourceRangeToJson(const SourceRange &range,
680 const FileManager &fileManager) {
681 AcavJson obj = AcavJson::object();
682 obj["begin"] = sourceLocationToJson(range.begin(), fileManager);
683 obj["end"] = sourceLocationToJson(range.end(), fileManager);
684 return obj;
685}
686
687bool parseSourceLocationJson(const AcavJson &obj, SourceLocation *out) {
688 if (!out || !obj.is_object()) {
689 return false;
690 }
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()) {
695 return false;
696 }
697 if (!fileIdIt->is_number_integer() || !lineIt->is_number_integer() ||
698 !columnIt->is_number_integer()) {
699 return false;
700 }
701
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) {
706 return false;
707 }
708
709 *out = SourceLocation(fileId, line, column);
710 return true;
711}
712
713bool parseSourceRangeJson(const AcavJson &obj, SourceRange *out) {
714 if (!out || !obj.is_object()) {
715 return false;
716 }
717 auto beginIt = obj.find("begin");
718 auto endIt = obj.find("end");
719 if (beginIt == obj.end() || endIt == obj.end()) {
720 return false;
721 }
722 SourceLocation begin(FileManager::InvalidFileID, 0, 0);
723 SourceLocation end(FileManager::InvalidFileID, 0, 0);
724 if (!parseSourceLocationJson(*beginIt, &begin) ||
725 !parseSourceLocationJson(*endIt, &end)) {
726 return false;
727 }
728 *out = SourceRange(begin, end);
729 return true;
730}
731
732AcavJson buildAstJsonTree(AstViewNode *root, const FileManager &fileManager) {
733 if (!root) {
734 return AcavJson();
735 }
736
737 auto makeNodeJson = [&fileManager](AstViewNode *node) {
738 AcavJson obj = AcavJson::object();
739 obj["properties"] = node->getProperties();
740 obj["sourceRange"] = sourceRangeToJson(node->getSourceRange(), fileManager);
741 return obj;
742 };
743
744 AcavJson rootJson = makeNodeJson(root);
745 std::vector<std::pair<AstViewNode *, AcavJson *>> stack;
746 stack.reserve(128);
747 stack.push_back({root, &rootJson});
748
749 while (!stack.empty()) {
750 auto [node, jsonPtr] = stack.back();
751 stack.pop_back();
752
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"];
758
759 for (AstViewNode *child : children) {
760 if (!child) {
761 continue;
762 }
763 AcavJson childJson = makeNodeJson(child);
764 storedChildren.push_back(childJson);
765 stack.push_back({child, &storedChildren.back()});
766 }
767 }
768
769 return rootJson;
770}
771
772} // namespace
773
774void MainWindow::setupDockWidgets() {
775
776 // Create views
777 tuView_ = new QTreeView(this);
778 sourceView_ = new SourceCodeView(this);
779 astView_ = new QTreeView(this);
780
781 // Create dock widgets with custom title bars for fast focus indication
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_);
787
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);
796
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);
805
806 setupTuSearch();
807
808 // Create Declaration Context dock (to the right of AST dock)
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_);
817
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) {
826 if (!logDockPtr) {
827 return;
828 }
829 LogEntry entry;
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));
836 });
837
838 // Layout: Top row (TU | Source | AST | DeclContext) with Log below
839 // Add all docks to the same area and split them for proper resize handles
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_);
845
846 // Now we have all the docks
847 // Begine to define the layout
848 // Split top docks horizontally: TU | Source | AST | DeclContext
849 splitDockWidget(tuDock_, sourceDock_, Qt::Horizontal);
850 splitDockWidget(sourceDock_, astDock_, Qt::Horizontal);
851 splitDockWidget(astDock_, declContextDock_, Qt::Horizontal);
852
853 // The whole window size: 1600x900
854 // Set initial sizes for top row (horizontal)
855 resizeDocks({tuDock_, sourceDock_, astDock_, declContextDock_},
856 {300, 450, 450, 200}, Qt::Horizontal);
857
858 // Set vertical distribution: top docks large, log dock small (~4 lines)
859 const int lineHeight = QFontMetrics(logDock_->font()).lineSpacing();
860 const int logDockHeight =
861 lineHeight * 4 + 70; // 4 lines + padding for tab bar + toolbar
862 const int topDockHeight =
863 900 - logDockHeight - 60; // Window height minus log and margins
864 resizeDocks({sourceDock_, logDock_}, {topDockHeight, logDockHeight},
865 Qt::Vertical);
866
867 setupViewMenuDockActions();
868 ::QTimer::singleShot(0, this, [this]() { defaultDockState_ = saveState(); });
869}
870
871void MainWindow::setupSourceSearchPanel(QWidget *container) {
872 if (!container) {
873 return;
874 }
875
876 auto *outerLayout = new QVBoxLayout(container);
877 outerLayout->setContentsMargins(4, 4, 4, 4);
878 outerLayout->setSpacing(4);
879
880 auto *controlsLayout = new QHBoxLayout();
881 controlsLayout->setContentsMargins(0, 0, 0, 0);
882 controlsLayout->setSpacing(4);
883
884 sourceSearchInput_ = new QLineEdit(container);
885 sourceSearchInput_->setPlaceholderText(tr("Search source code..."));
886 sourceSearchInput_->setClearButtonEnabled(true);
887 sourceSearchInput_->setProperty("searchField", true);
888 sourceSearchInput_->setMinimumHeight(28);
889
890 sourceSearchPrevButton_ = new QToolButton(container);
891 sourceSearchPrevButton_->setText(tr("Prev"));
892 sourceSearchPrevButton_->setEnabled(false);
893 sourceSearchPrevButton_->setProperty("searchButton", true);
894 sourceSearchPrevButton_->setMinimumHeight(28);
895
896 sourceSearchNextButton_ = new QToolButton(container);
897 sourceSearchNextButton_->setText(tr("Next"));
898 sourceSearchNextButton_->setEnabled(false);
899 sourceSearchNextButton_->setProperty("searchButton", true);
900 sourceSearchNextButton_->setMinimumHeight(28);
901
902 sourceSearchStatus_ = new QLabel(container);
903 sourceSearchStatus_->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
904 sourceSearchStatus_->setMinimumWidth(80);
905 sourceSearchStatus_->setProperty("searchStatus", true);
906
907 sourceSearchDebounce_ = new ::QTimer(container);
908 sourceSearchDebounce_->setSingleShot(true);
909 sourceSearchDebounce_->setInterval(200);
910
911 controlsLayout->addWidget(sourceSearchInput_, /*stretch=*/1);
912 controlsLayout->addWidget(sourceSearchPrevButton_);
913 controlsLayout->addWidget(sourceSearchNextButton_);
914 controlsLayout->addWidget(sourceSearchStatus_);
915
916 outerLayout->addLayout(controlsLayout);
917 outerLayout->addWidget(sourceView_, /*stretch=*/1);
918
919 connect(sourceSearchInput_, &QLineEdit::returnPressed, this,
920 &MainWindow::onSourceSearchFindNext);
921 connect(sourceSearchInput_, &QLineEdit::textChanged, this,
922 &MainWindow::onSourceSearchTextChanged);
923 connect(sourceSearchDebounce_, &::QTimer::timeout, this,
924 &MainWindow::onSourceSearchDebounced);
925 connect(sourceSearchPrevButton_, &QToolButton::clicked, this,
926 &MainWindow::onSourceSearchFindPrevious);
927 connect(sourceSearchNextButton_, &QToolButton::clicked, this,
928 &MainWindow::onSourceSearchFindNext);
929}
930
931void MainWindow::setupAstSearchPanel(QWidget *container) {
932 if (!container) {
933 return;
934 }
935
936 auto *outerLayout = new QVBoxLayout(container);
937 outerLayout->setContentsMargins(8, 8, 8, 8);
938 outerLayout->setSpacing(8);
939
940 // === Quick Search Section with Enhanced Visual Design ===
941 auto *quickSearchFrame = new QWidget(container);
942 quickSearchFrame->setObjectName("astQuickSearchFrame");
943
944 auto *quickLayout = new QHBoxLayout(quickSearchFrame);
945 quickLayout->setContentsMargins(8, 6, 8, 6);
946 quickLayout->setSpacing(6);
947
948 astSearchQuickInput_ = new QLineEdit(quickSearchFrame);
949 astSearchQuickInput_->setObjectName("astSearchQuickInput");
950 astSearchQuickInput_->setPlaceholderText(tr("Search AST nodes..."));
951 astSearchQuickInput_->setClearButtonEnabled(true);
952 astSearchQuickInput_->setProperty("searchField", true);
953 astSearchQuickInput_->setMinimumHeight(28);
954
955 auto *quickSearchButton = new QToolButton(quickSearchFrame);
956 quickSearchButton->setObjectName("astSearchQuickButton");
957 quickSearchButton->setText(tr("Advanced"));
958 quickSearchButton->setToolTip(tr("Open advanced search options (Ctrl+F)"));
959 quickSearchButton->setProperty("searchButton", true);
960 quickSearchButton->setMinimumHeight(28);
961 quickSearchButton->setMinimumWidth(90);
962
963 quickLayout->addWidget(astSearchQuickInput_, /*stretch=*/1);
964 quickLayout->addWidget(quickSearchButton);
965
966 // === Advanced Search Popup with Modern Design ===
967 astSearchPopup_ = new QDialog(this);
968 astSearchPopup_->setObjectName("astSearchPopup");
969 astSearchPopup_->setWindowTitle(tr("AST Advanced Search"));
970 astSearchPopup_->setModal(false);
971 astSearchPopup_->setWindowModality(Qt::NonModal);
972 astSearchPopup_->setMinimumSize(320, 180);
973 astSearchPopup_->resize(600, 220);
974
975 auto *popupLayout = new QVBoxLayout(astSearchPopup_);
976 popupLayout->setContentsMargins(12, 12, 12, 12);
977 popupLayout->setSpacing(10);
978
979 // Search input with improved styling
980 auto *inputLabel = new QLabel(tr("<b>Search Pattern:</b>"), astSearchPopup_);
981 popupLayout->addWidget(inputLabel);
982
983 astSearchInput_ = new HistoryLineEdit(astSearchPopup_);
984 astSearchInput_->setObjectName("astSearchInput");
985 astSearchInput_->setPlaceholderText(
986 tr("e.g., kind:FunctionDecl name:main type:.*int.*"));
987 astSearchInput_->setClearButtonEnabled(true);
988 astSearchInput_->setProperty("searchField", true);
989 astSearchInput_->setMinimumHeight(32);
990 astSearchInput_->setToolTip(
991 tr("Use qualifiers like kind:, name:, type: for precise matching.\n"
992 "Supports regex patterns.\n"
993 "Examples: kind:FunctionDecl name:main type:.*int.*"));
994
995 astSearchHistoryModel_ = new QStringListModel(astSearchPopup_);
996 astSearchCompleter_ = new QCompleter(astSearchHistoryModel_, astSearchPopup_);
997 astSearchCompleter_->setCaseSensitivity(Qt::CaseInsensitive);
998 astSearchCompleter_->setCompletionMode(QCompleter::PopupCompletion);
999 astSearchCompleter_->setFilterMode(Qt::MatchContains);
1000 astSearchInput_->setCompleter(astSearchCompleter_);
1001
1002 popupLayout->addWidget(astSearchInput_);
1003
1004 // Separator line
1005 auto *separator = new QFrame(astSearchPopup_);
1006 separator->setFrameShape(QFrame::HLine);
1007 separator->setFrameShadow(QFrame::Sunken);
1008 popupLayout->addWidget(separator);
1009
1010 // Controls section with better organization
1011 auto *controlsFrame = new QWidget(astSearchPopup_);
1012 auto *popupControlsLayout = new QHBoxLayout(controlsFrame);
1013 popupControlsLayout->setContentsMargins(0, 0, 0, 0);
1014 popupControlsLayout->setSpacing(8);
1015
1016 // Project filter
1017 astSearchProjectFilter_ = new QCheckBox(tr("Project Files Only"), astSearchPopup_);
1018 astSearchProjectFilter_->setChecked(true);
1019 astSearchProjectFilter_->setToolTip(tr("Restrict search to files within the project root"));
1020
1021 popupControlsLayout->addWidget(astSearchProjectFilter_);
1022 popupControlsLayout->addStretch(1);
1023
1024 // Navigation buttons with better styling
1025 astSearchPrevButton_ = new QToolButton(astSearchPopup_);
1026 astSearchPrevButton_->setText(tr("Previous"));
1027 astSearchPrevButton_->setToolTip(tr("Find previous match (Shift+Enter)"));
1028 astSearchPrevButton_->setEnabled(false);
1029 astSearchPrevButton_->setProperty("searchButton", true);
1030 astSearchPrevButton_->setMinimumWidth(90);
1031 astSearchPrevButton_->setMinimumHeight(28);
1032
1033 astSearchNextButton_ = new QToolButton(astSearchPopup_);
1034 astSearchNextButton_->setText(tr("Next"));
1035 astSearchNextButton_->setToolTip(tr("Find next match (Enter)"));
1036 astSearchNextButton_->setEnabled(false);
1037 astSearchNextButton_->setProperty("searchButton", true);
1038 astSearchNextButton_->setMinimumWidth(90);
1039 astSearchNextButton_->setMinimumHeight(28);
1040
1041 auto *astSearchButton = new QToolButton(astSearchPopup_);
1042 astSearchButton->setText(tr("Search"));
1043 astSearchButton->setToolTip(tr("Execute search"));
1044 astSearchButton->setProperty("searchButton", true);
1045 astSearchButton->setMinimumWidth(90);
1046 astSearchButton->setMinimumHeight(28);
1047
1048 popupControlsLayout->addWidget(astSearchPrevButton_);
1049 popupControlsLayout->addWidget(astSearchNextButton_);
1050 popupControlsLayout->addWidget(astSearchButton);
1051
1052 popupLayout->addWidget(controlsFrame);
1053
1054 // Status bar at the bottom
1055 astSearchStatus_ = new QLabel(astSearchPopup_);
1056 astSearchStatus_->setObjectName("astSearchStatus");
1057 astSearchStatus_->setAlignment(Qt::AlignCenter);
1058 astSearchStatus_->setMinimumHeight(24);
1059 astSearchStatus_->setProperty("searchStatus", true);
1060 popupLayout->addWidget(astSearchStatus_);
1061
1062 // === Compilation Warning Banner ===
1063 astCompilationWarningLabel_ = new QLabel(container);
1064 astCompilationWarningLabel_->setObjectName("astCompilationWarningLabel");
1065 astCompilationWarningLabel_->setText(
1066 tr("<b>Compilation Errors Detected:</b> AST may be incomplete. "
1067 "See log for details."));
1068 astCompilationWarningLabel_->setTextFormat(Qt::RichText);
1069 astCompilationWarningLabel_->setWordWrap(true);
1070 // Styled by #astCompilationWarningLabel in style.qss
1071 astCompilationWarningLabel_->setVisible(false);
1072
1073 outerLayout->addWidget(quickSearchFrame);
1074 outerLayout->addWidget(astCompilationWarningLabel_);
1075 outerLayout->addWidget(astView_, /*stretch=*/1);
1076
1077 connect(astSearchQuickInput_, &QLineEdit::textChanged, this,
1078 [this](const QString &text) {
1079 if (astSearchInput_ && astSearchInput_->text() != text) {
1080 astSearchInput_->setText(text);
1081 }
1082 });
1083 auto triggerQuickSearch = [this]() {
1084 if (!astSearchInput_ || !astSearchQuickInput_) {
1085 return;
1086 }
1087 const QString query = astSearchQuickInput_->text();
1088 if (astSearchInput_->text() != query) {
1089 astSearchInput_->setText(query);
1090 }
1091 showAstSearchPopup(false);
1092 onAstSearchFindNext();
1093 };
1094 connect(astSearchQuickInput_, &QLineEdit::returnPressed, this,
1095 triggerQuickSearch);
1096 connect(quickSearchButton, &QToolButton::clicked, this, triggerQuickSearch);
1097 connect(astSearchInput_, &QLineEdit::returnPressed, this,
1098 &MainWindow::onAstSearchFindNext);
1099 connect(astSearchInput_, &QLineEdit::textChanged, this,
1100 &MainWindow::onAstSearchTextChanged);
1101 connect(astSearchInput_, &QLineEdit::textChanged, this,
1102 [this](const QString &text) {
1103 if (astSearchQuickInput_ && astSearchQuickInput_->text() != text) {
1104 astSearchQuickInput_->setText(text);
1105 }
1106 });
1107 connect(astSearchButton, &QToolButton::clicked, this,
1108 &MainWindow::onAstSearchFindNext);
1109 connect(astSearchPrevButton_, &QToolButton::clicked, this,
1110 &MainWindow::onAstSearchFindPrevious);
1111 connect(astSearchNextButton_, &QToolButton::clicked, this,
1112 &MainWindow::onAstSearchFindNext);
1113 connect(astSearchProjectFilter_, &QCheckBox::toggled, this,
1114 &MainWindow::clearAstSearchState);
1115
1116 auto *prevShortcut =
1117 new QShortcut(QKeySequence(Qt::SHIFT | Qt::Key_Return), astSearchPopup_);
1118 connect(prevShortcut, &QShortcut::activated, this,
1119 &MainWindow::onAstSearchFindPrevious);
1120 auto *prevNumpadShortcut =
1121 new QShortcut(QKeySequence(Qt::SHIFT | Qt::Key_Enter), astSearchPopup_);
1122 connect(prevNumpadShortcut, &QShortcut::activated, this,
1123 &MainWindow::onAstSearchFindPrevious);
1124 auto *closeShortcut = new QShortcut(QKeySequence(Qt::Key_Escape),
1125 astSearchPopup_);
1126 connect(closeShortcut, &QShortcut::activated, astSearchPopup_,
1127 &QDialog::hide);
1128
1129 if (astDock_) {
1130 astDock_->installEventFilter(this);
1131 }
1132
1133 // Ensure popup starts with configured base font size.
1134 QFont popupFont = QApplication::font();
1135 popupFont.setPointSize(AppConfig::instance().getFontSize());
1136 QString configuredFamily = AppConfig::instance().getFontFamily();
1137 if (!configuredFamily.isEmpty()) {
1138 popupFont.setFamily(configuredFamily);
1139 }
1140 astSearchPopup_->setFont(popupFont);
1141 if (astSearchCompleter_ && astSearchCompleter_->popup()) {
1142 astSearchCompleter_->popup()->setFont(popupFont);
1143 }
1144
1145 astSearchPopup_->hide();
1146}
1147
1148void MainWindow::setupTuSearch() {
1149 tuSearch_ = new QLineEdit(this);
1150 tuSearch_->setPlaceholderText(tr("Filter files..."));
1151 tuSearch_->setClearButtonEnabled(true);
1152 tuSearch_->setProperty("searchField", true);
1153 tuSearch_->setMinimumHeight(28);
1154 connect(tuSearch_, &QLineEdit::textChanged, this,
1155 [this](const QString &text) {
1156 QString needle = text.trimmed();
1157
1158 // Suspend updates during batch setRowHidden calls to avoid
1159 // O(N) layout recalculations (one per row).
1160 tuView_->setUpdatesEnabled(false);
1161
1162 // Recursive filter that shows parents when children match
1163 std::function<bool(const QModelIndex &)> filterRecursive =
1164 [&](const QModelIndex &parent) -> bool {
1165 bool anyChildVisible = false;
1166 int rowCount = tuModel_->rowCount(parent);
1167
1168 for (int i = 0; i < rowCount; ++i) {
1169 QModelIndex idx = tuModel_->index(i, 0, parent);
1170 QString name = tuModel_->data(idx, Qt::DisplayRole).toString();
1171
1172 // Check if this item matches
1173 bool matches = needle.isEmpty() ||
1174 name.contains(needle, Qt::CaseInsensitive);
1175
1176 // Recursively check children
1177 bool childVisible = filterRecursive(idx);
1178
1179 // Item is visible if it matches or has visible children
1180 bool visible = matches || childVisible;
1181 tuView_->setRowHidden(i, parent, !visible);
1182
1183 if (visible) {
1184 anyChildVisible = true;
1185 }
1186 }
1187
1188 return anyChildVisible;
1189 };
1190
1191 filterRecursive(QModelIndex());
1192
1193 tuView_->setUpdatesEnabled(true);
1194 });
1195
1196 // Place the search above the TU tree
1197 QWidget *container = new QWidget(tuDock_);
1198 QVBoxLayout *layout = new QVBoxLayout(container);
1199 layout->setContentsMargins(4, 4, 4, 4);
1200 layout->setSpacing(4);
1201 layout->addWidget(tuSearch_);
1202 layout->addWidget(tuView_);
1203 tuDock_->setWidget(container);
1204}
1205
1206void MainWindow::triggerSourceSearch(bool forward) {
1207 if (!sourceView_ || !sourceSearchInput_) {
1208 return;
1209 }
1210
1211 const QString term = sourceSearchInput_->text();
1212 if (term.trimmed().isEmpty()) {
1213 if (sourceSearchStatus_) {
1214 sourceSearchStatus_->setText(tr("Enter text"));
1215 }
1216 return;
1217 }
1218
1219 bool found =
1220 forward ? sourceView_->findNext(term) : sourceView_->findPrevious(term);
1221 if (sourceSearchStatus_) {
1222 sourceSearchStatus_->setText(found ? QString() : tr("No matches"));
1223 }
1224}
1225
1226void MainWindow::onSourceSearchTextChanged(const QString &text) {
1227 const bool hasText = !text.trimmed().isEmpty();
1228
1229 // These pointers are set up in setupSourceSearchPanel and always valid
1230 sourceSearchPrevButton_->setEnabled(hasText);
1231 sourceSearchNextButton_->setEnabled(hasText);
1232 sourceView_->clearSearchHighlight();
1233 sourceSearchStatus_->clear();
1234
1235 if (hasText) {
1236 sourceSearchDebounce_->start();
1237 } else {
1238 sourceSearchDebounce_->stop();
1239 }
1240}
1241
1242void MainWindow::onSourceSearchDebounced() { triggerSourceSearch(true); }
1243
1244void MainWindow::onSourceSearchFindNext() {
1245 if (sourceSearchDebounce_) {
1246 sourceSearchDebounce_->stop();
1247 }
1248 triggerSourceSearch(true);
1249}
1250
1251void MainWindow::onSourceSearchFindPrevious() {
1252 if (sourceSearchDebounce_) {
1253 sourceSearchDebounce_->stop();
1254 }
1255 triggerSourceSearch(false);
1256}
1257
1258void MainWindow::onAstSearchTextChanged(const QString &text) {
1259 const bool hasText = !text.trimmed().isEmpty();
1260
1261 astSearchPrevButton_->setEnabled(hasText);
1262 astSearchNextButton_->setEnabled(hasText);
1263 clearAstSearchState();
1264}
1265
1266void MainWindow::onAstSearchDebounced() { triggerAstSearch(true); }
1267
1268void MainWindow::onAstSearchFindNext() { triggerAstSearch(true); }
1269
1270void MainWindow::onAstSearchFindPrevious() { triggerAstSearch(false); }
1271
1272void MainWindow::triggerAstSearch(bool forward) {
1273 if (!astSearchInput_) {
1274 return;
1275 }
1276
1277 const QString expression = astSearchInput_->text().trimmed();
1278 if (expression.isEmpty()) {
1279 return;
1280 }
1281 rememberAstSearchQuery(expression);
1282
1283 if (!astModel_ || !astModel_->hasNodes()) {
1284 setAstSearchStatus(tr("No AST loaded"), true);
1285 return;
1286 }
1287
1288 if (astSearchMatches_.empty()) {
1289 collectAstSearchMatches(expression);
1290 // collectAstSearchMatches sets status on invalid regex
1291 if (astSearchMatches_.empty()) {
1292 if (astSearchStatus_ &&
1293 astSearchStatus_->text() != tr("Invalid pattern")) {
1294 setAstSearchStatus(tr("No matches"), false);
1295 }
1296 return;
1297 }
1298 // First search: start at first match for forward, last for backward
1299 astSearchCurrentIndex_ =
1300 forward ? 0 : static_cast<int>(astSearchMatches_.size()) - 1;
1301 } else {
1302 int count = static_cast<int>(astSearchMatches_.size());
1303 if (forward) {
1304 astSearchCurrentIndex_ = (astSearchCurrentIndex_ + 1) % count;
1305 } else {
1306 astSearchCurrentIndex_ = (astSearchCurrentIndex_ - 1 + count) % count;
1307 }
1308 }
1309
1310 navigateToAstMatch(astSearchCurrentIndex_);
1311 setAstSearchStatus(QString("%1 of %2")
1312 .arg(astSearchCurrentIndex_ + 1)
1313 .arg(astSearchMatches_.size()),
1314 false);
1315}
1316
1317void MainWindow::collectAstSearchMatches(const QString &expression) {
1318 astSearchMatches_.clear();
1319 astSearchCurrentIndex_ = -1;
1320
1321 struct AstSearchCondition {
1322 QString key;
1323 QRegularExpression regex;
1324 };
1325
1326 // Parse expression: scan for qualifier boundaries (word:)
1327 std::vector<AstSearchCondition> conditions;
1328 QRegularExpression qualifierPattern(QStringLiteral("(\\w+):"));
1329 auto it = qualifierPattern.globalMatch(expression);
1330
1331 // Collect qualifier positions
1332 struct QualifierPos {
1333 int start; // start of "key:"
1334 int valueStart; // position after ":"
1335 QString key;
1336 };
1337 std::vector<QualifierPos> qualifiers;
1338 while (it.hasNext()) {
1339 auto match = it.next();
1340 qualifiers.push_back({static_cast<int>(match.capturedStart()),
1341 static_cast<int>(match.capturedEnd()),
1342 match.captured(1)});
1343 }
1344
1345 if (qualifiers.empty()) {
1346 // Entire expression is bare text
1347 QRegularExpression regex(expression,
1348 QRegularExpression::CaseInsensitiveOption);
1349 if (!regex.isValid()) {
1350 setAstSearchStatus(tr("Invalid pattern"), true);
1351 return;
1352 }
1353 conditions.push_back({QString(), std::move(regex)});
1354 } else {
1355 // Text before first qualifier is bare text
1356 if (qualifiers.front().start > 0) {
1357 QString bareText = expression.left(qualifiers.front().start).trimmed();
1358 if (!bareText.isEmpty()) {
1359 QRegularExpression regex(bareText,
1360 QRegularExpression::CaseInsensitiveOption);
1361 if (!regex.isValid()) {
1362 setAstSearchStatus(tr("Invalid pattern"), true);
1363 return;
1364 }
1365 conditions.push_back({QString(), std::move(regex)});
1366 }
1367 }
1368
1369 // Process each qualifier
1370 for (std::size_t i = 0; i < qualifiers.size(); ++i) {
1371 int valueStart = qualifiers[i].valueStart;
1372 int valueEnd = (i + 1 < qualifiers.size()) ? qualifiers[i + 1].start
1373 : expression.size();
1374 QString value =
1375 expression.mid(valueStart, valueEnd - valueStart).trimmed();
1376 // Anchor qualified searches for exact matching:
1377 // name:main matches "main" but not "remainder"
1378 QString anchored =
1379 value.isEmpty() ? value : QStringLiteral("^(?:%1)$").arg(value);
1380 QRegularExpression regex(anchored,
1381 QRegularExpression::CaseInsensitiveOption);
1382 if (!regex.isValid()) {
1383 setAstSearchStatus(tr("Invalid pattern"), true);
1384 return;
1385 }
1386 conditions.push_back({qualifiers[i].key, std::move(regex)});
1387 }
1388 }
1389
1390 // Project-only filter state
1391 QString projectRoot = tuModel_ ? tuModel_->projectRoot() : QString();
1392 bool projectFilterActive =
1393 astSearchProjectFilter_ && astSearchProjectFilter_->isChecked();
1394 std::unordered_map<FileID, bool> projectFileCache;
1395
1396 // DFS traversal over AST (iterative pre-order)
1397 AstViewNode *root = astModel_->selectedNode();
1398 // We need the actual tree root, not the selected node
1399 // Access root via the model's index(0,0) which gives the first top-level node
1400 QModelIndex rootIndex = astModel_->index(0, 0);
1401 if (!rootIndex.isValid()) {
1402 return;
1403 }
1404 root = static_cast<AstViewNode *>(
1405 rootIndex.data(AstModel::NodePtrRole).value<void *>());
1406 if (!root) {
1407 return;
1408 }
1409 // The root in the model is the actual root - walk from there
1410 // But we need to go up to the true root (parent of root index)
1411 // Actually, the model's root_ is not directly accessible. We traverse
1412 // starting from the model's top-level children.
1413 // Let's collect all top-level nodes and DFS from each.
1414 std::vector<AstViewNode *> stack;
1415 int topLevelCount = astModel_->rowCount();
1416 for (int i = topLevelCount - 1; i >= 0; --i) {
1417 QModelIndex idx = astModel_->index(i, 0);
1418 if (idx.isValid()) {
1419 auto *node = static_cast<AstViewNode *>(
1420 idx.data(AstModel::NodePtrRole).value<void *>());
1421 if (node) {
1422 stack.push_back(node);
1423 }
1424 }
1425 }
1426
1427 auto jsonValueToString = [](const AcavJson &value) -> QString {
1428 if (value.is_boolean()) {
1429 return value.get<bool>() ? QStringLiteral("true")
1430 : QStringLiteral("false");
1431 }
1432 if (value.is_number_integer()) {
1433 return QString::number(value.get<int64_t>());
1434 }
1435 if (value.is_number_unsigned()) {
1436 return QString::number(value.get<uint64_t>());
1437 }
1438 if (value.is_number_float()) {
1439 return QString::number(value.get<double>());
1440 }
1441 if (value.is_string()) {
1442 return QString::fromStdString(value.get<InternedString>().str());
1443 }
1444 return {};
1445 };
1446
1447 while (!stack.empty()) {
1448 AstViewNode *node = stack.back();
1449 stack.pop_back();
1450
1451 // Project-only filter: skip nodes from external files
1452 if (projectFilterActive) {
1453 FileID fileId = node->getSourceRange().begin().fileID();
1454 auto cacheIt = projectFileCache.find(fileId);
1455 if (cacheIt == projectFileCache.end()) {
1456 bool isProject = false;
1457 if (fileId != FileManager::InvalidFileID && !projectRoot.isEmpty()) {
1458 std::string_view path = fileManager_.getFilePath(fileId);
1459 if (!path.empty()) {
1460 QString qpath =
1461 QString::fromUtf8(path.data(), static_cast<int>(path.size()));
1462 isProject = (qpath == projectRoot) ||
1463 qpath.startsWith(projectRoot + QChar('/'));
1464 }
1465 }
1466 cacheIt = projectFileCache.emplace(fileId, isProject).first;
1467 }
1468 if (!cacheIt->second) {
1469 // Still traverse children -- they may be in project files
1470 const auto &children = node->getChildren();
1471 for (auto childIt = children.rbegin(); childIt != children.rend();
1472 ++childIt) {
1473 if (*childIt)
1474 stack.push_back(*childIt);
1475 }
1476 continue;
1477 }
1478 }
1479
1480 const auto &props = node->getProperties();
1481 bool allMatch = true;
1482
1483 for (const auto &cond : conditions) {
1484 if (cond.key.isEmpty()) {
1485 // Bare text: search all properties on the node
1486 bool anyPropMatch = false;
1487 if (props.is_object()) {
1488 for (auto it = props.begin(); it != props.end(); ++it) {
1489 QString val = jsonValueToString(*it);
1490 if (!val.isEmpty() && cond.regex.match(val).hasMatch()) {
1491 anyPropMatch = true;
1492 break;
1493 }
1494 }
1495 }
1496 if (!anyPropMatch) {
1497 allMatch = false;
1498 break;
1499 }
1500 } else {
1501 // Qualified: check specific property
1502 QByteArray keyUtf8 = cond.key.toUtf8();
1503 const char *keyStr = keyUtf8.constData();
1504 auto propIt = props.find(keyStr);
1505 if (propIt == props.end()) {
1506 allMatch = false;
1507 break;
1508 }
1509 QString val = jsonValueToString(*propIt);
1510 if (!cond.regex.match(val).hasMatch()) {
1511 allMatch = false;
1512 break;
1513 }
1514 }
1515 }
1516
1517 if (allMatch) {
1518 astSearchMatches_.push_back(node);
1519 }
1520
1521 // Push children in reverse order for pre-order traversal
1522 const auto &children = node->getChildren();
1523 for (auto childIt = children.rbegin(); childIt != children.rend();
1524 ++childIt) {
1525 if (*childIt) {
1526 stack.push_back(*childIt);
1527 }
1528 }
1529 }
1530}
1531
1532void MainWindow::navigateToAstMatch(int index) {
1533 if (index < 0 || index >= static_cast<int>(astSearchMatches_.size())) {
1534 return;
1535 }
1536 AstViewNode *node = astSearchMatches_[index];
1537 QModelIndex modelIndex = astModel_->selectNode(node);
1538 astView_->setCurrentIndex(modelIndex);
1539 astView_->scrollTo(modelIndex);
1540}
1541
1542void MainWindow::clearAstSearchState() {
1543 astSearchMatches_.clear();
1544 astSearchCurrentIndex_ = -1;
1545 if (astSearchStatus_) {
1546 astSearchStatus_->clear();
1547 astSearchStatus_->setProperty("searchStatus", true);
1548 astSearchStatus_->style()->unpolish(astSearchStatus_);
1549 astSearchStatus_->style()->polish(astSearchStatus_);
1550 }
1551}
1552
1553void MainWindow::setAstSearchStatus(const QString &text, bool isError) {
1554 if (!astSearchStatus_) {
1555 return;
1556 }
1557 astSearchStatus_->setText(text);
1558
1559 // Toggle between normal/error appearance via dynamic property.
1560 // The QSS selectors [searchStatus="true"] and [searchStatus="error"]
1561 // handle the actual visual styling (see style.qss).
1562 if (isError) {
1563 astSearchStatus_->setProperty("searchStatus", QStringLiteral("error"));
1564 } else {
1565 astSearchStatus_->setProperty("searchStatus", true);
1566 }
1567 astSearchStatus_->style()->unpolish(astSearchStatus_);
1568 astSearchStatus_->style()->polish(astSearchStatus_);
1569}
1570
1571void MainWindow::showAstSearchPopup(bool selectAll) {
1572 if (!astSearchPopup_ || !astSearchInput_) {
1573 return;
1574 }
1575
1576 syncAstSearchPopupGeometry();
1577 astSearchPopup_->show();
1578
1579 astSearchPopup_->raise();
1580 astSearchPopup_->activateWindow();
1581 astSearchInput_->setFocus();
1582 if (selectAll) {
1583 astSearchInput_->selectAll();
1584 }
1585}
1586
1587void MainWindow::syncAstSearchPopupGeometry() {
1588 if (!astSearchPopup_) {
1589 return;
1590 }
1591
1592 const int minWidth = astSearchPopup_->minimumWidth();
1593 const int maxWidth = 900;
1594 int targetWidth = astSearchPopup_->width();
1595 QPoint anchor(0, 0);
1596
1597 if (astDock_) {
1598 const int availableWidth = qMax(minWidth, astDock_->width() - 16);
1599 const int preferredWidth = static_cast<int>(availableWidth * 0.85);
1600 targetWidth = qBound(minWidth, preferredWidth, qMin(maxWidth, availableWidth));
1601 const int x = qMax(8, astDock_->width() - targetWidth - 8);
1602 anchor = astDock_->mapToGlobal(QPoint(x, 40));
1603 } else {
1604 const int availableWidth = qMax(minWidth, width() - 32);
1605 const int preferredWidth = static_cast<int>(availableWidth * 0.55);
1606 targetWidth = qBound(minWidth, preferredWidth, qMin(maxWidth, availableWidth));
1607 anchor = mapToGlobal(QPoint(qMax(8, width() - targetWidth - 16), 40));
1608 }
1609
1610 int targetHeight = qMax(astSearchPopup_->sizeHint().height(),
1611 astSearchPopup_->minimumHeight());
1612 astSearchPopup_->resize(targetWidth, targetHeight);
1613 astSearchPopup_->move(anchor);
1614}
1615
1616void MainWindow::rememberAstSearchQuery(const QString &query) {
1617 const QString trimmed = query.trimmed();
1618 if (trimmed.isEmpty()) {
1619 return;
1620 }
1621
1622 astSearchHistory_.removeAll(trimmed);
1623 astSearchHistory_.prepend(trimmed);
1624
1625 constexpr int kMaxQueryHistory = 20;
1626 while (astSearchHistory_.size() > kMaxQueryHistory) {
1627 astSearchHistory_.removeLast();
1628 }
1629
1630 if (astSearchHistoryModel_) {
1631 astSearchHistoryModel_->setStringList(astSearchHistory_);
1632 }
1633}
1634
1635void MainWindow::setAstCompilationWarningVisible(bool visible) {
1636 if (!astCompilationWarningLabel_) {
1637 return;
1638 }
1639 astCompilationWarningLabel_->setVisible(visible);
1640}
1641
1642void MainWindow::applyFontSize(int size) {
1643 // Validate and clamp font size to valid range
1644 if (size < kMinFontSize) {
1645 size = kMinFontSize;
1646 } else if (size > kMaxFontSize) {
1647 size = kMaxFontSize;
1648 }
1649 currentFontSize_ = size;
1650 tuFontSize_ = size;
1651 sourceFontSize_ = size;
1652 astFontSize_ = size;
1653 declContextFontSize_ = size;
1654 logFontSize_ = size;
1655 currentFontFamily_ = AppConfig::instance().getFontFamily();
1656
1657 QFont baseFont = QApplication::font();
1658 if (!currentFontFamily_.isEmpty()) {
1659 baseFont.setFamily(currentFontFamily_);
1660 }
1661 baseFont.setPointSize(size);
1662
1663 auto applyFont = [&baseFont](QWidget *widget) {
1664 if (!widget) {
1665 return;
1666 }
1667 widget->setFont(baseFont);
1668 };
1669
1670 applyFont(tuView_);
1671 applyFont(astView_);
1672 if (declContextView_) {
1673 declContextView_->applyFont(baseFont);
1674 }
1675 applyFont(logDock_);
1676 applyFont(nodeCycleWidget_);
1677 applyFont(astSearchQuickInput_);
1678 applyFont(astSearchPopup_);
1679 if (astSearchCompleter_ && astSearchCompleter_->popup()) {
1680 astSearchCompleter_->popup()->setFont(baseFont);
1681 }
1682 if (sourceView_) {
1683 sourceView_->setFont(baseFont);
1684 sourceView_->applyFontSize(size);
1685 }
1686}
1687
1688void MainWindow::adjustFontSize(int delta) {
1689 // Per-subwindow font size: adjust only the focused dock
1690 currentFontFamily_ = AppConfig::instance().getFontFamily();
1691 QFont baseFont = QApplication::font();
1692 if (!currentFontFamily_.isEmpty()) {
1693 baseFont.setFamily(currentFontFamily_);
1694 }
1695
1696 auto adjustWidget = [&](QWidget *widget, int *fontSize) {
1697 if (!widget || !fontSize) {
1698 return;
1699 }
1700 int nextSize = *fontSize + delta;
1701 if (nextSize < kMinFontSize) {
1702 nextSize = kMinFontSize;
1703 } else if (nextSize > kMaxFontSize) {
1704 nextSize = kMaxFontSize;
1705 }
1706 if (nextSize == *fontSize) {
1707 return;
1708 }
1709 *fontSize = nextSize;
1710 baseFont.setPointSize(nextSize);
1711 widget->setFont(baseFont);
1712 };
1713
1714 if (focusedDock_ == tuDock_) {
1715 adjustWidget(tuView_, &tuFontSize_);
1716 } else if (focusedDock_ == sourceDock_) {
1717 int nextSize = sourceFontSize_ + delta;
1718 if (nextSize < kMinFontSize) {
1719 nextSize = kMinFontSize;
1720 } else if (nextSize > kMaxFontSize) {
1721 nextSize = kMaxFontSize;
1722 }
1723 if (nextSize != sourceFontSize_ && sourceView_) {
1724 sourceFontSize_ = nextSize;
1725 baseFont.setPointSize(nextSize);
1726 sourceView_->setFont(baseFont);
1727 sourceView_->applyFontSize(nextSize);
1728 }
1729 } else if (focusedDock_ == astDock_) {
1730 adjustWidget(astView_, &astFontSize_);
1731 baseFont.setPointSize(astFontSize_);
1732 if (astSearchQuickInput_) {
1733 astSearchQuickInput_->setFont(baseFont);
1734 }
1735 if (astSearchPopup_) {
1736 astSearchPopup_->setFont(baseFont);
1737 }
1738 if (astSearchCompleter_ && astSearchCompleter_->popup()) {
1739 astSearchCompleter_->popup()->setFont(baseFont);
1740 }
1741 } else if (focusedDock_ == declContextDock_) {
1742 int nextSize = declContextFontSize_ + delta;
1743 if (nextSize < kMinFontSize) {
1744 nextSize = kMinFontSize;
1745 } else if (nextSize > kMaxFontSize) {
1746 nextSize = kMaxFontSize;
1747 }
1748 if (nextSize != declContextFontSize_ && declContextView_) {
1749 declContextFontSize_ = nextSize;
1750 baseFont.setPointSize(nextSize);
1751 declContextView_->applyFont(baseFont);
1752 }
1753 } else if (focusedDock_ == logDock_) {
1754 adjustWidget(logDock_, &logFontSize_);
1755 } else {
1756 // Fallback: apply to all (e.g., when no dock focused)
1757 int nextSize = currentFontSize_ + delta;
1758 if (nextSize < kMinFontSize) {
1759 nextSize = kMinFontSize;
1760 } else if (nextSize > kMaxFontSize) {
1761 nextSize = kMaxFontSize;
1762 }
1763 if (nextSize != currentFontSize_) {
1764 applyFontSize(nextSize);
1765 }
1766 }
1767}
1768
1769void MainWindow::expandFileExplorerTopLevel() {
1770 if (!tuView_ || !tuModel_) {
1771 return;
1772 }
1773
1774 // Expand top-level items (Project Files, External Files)
1775 int topLevelCount = tuModel_->rowCount();
1776 for (int i = 0; i < topLevelCount; ++i) {
1777 QModelIndex topLevelIndex = tuModel_->index(i, 0);
1778 tuView_->expand(topLevelIndex);
1779 }
1780}
1781
1782void MainWindow::setupModels() {
1783 // Create models
1784 tuModel_ = new TranslationUnitModel(fileManager_, this);
1785 astModel_ = new AstModel(this);
1786
1787 // Set models to views
1788 tuView_->setModel(tuModel_);
1789 astView_->setModel(astModel_);
1790
1791 // Configure tree views for performance
1792 auto configureTreeView = [](QTreeView *view) {
1793 view->setHeaderHidden(true);
1794 view->setAnimated(false);
1795 view->setUniformRowHeights(true);
1796 view->setTextElideMode(Qt::ElideNone);
1797 view->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
1798 view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
1799 view->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
1800 view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
1801 view->header()->setStretchLastSection(false);
1802 // Use ResizeToContents so the column auto-sizes to the widest node.
1803 // Batch expand/collapse operations temporarily switch to Fixed mode
1804 // to avoid O(N) width recalculation on every individual expand/collapse.
1805 view->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
1806 };
1807 configureTreeView(tuView_);
1808 configureTreeView(astView_);
1809
1810 // Create query runners
1811 queryRunner_ = new QueryDependenciesRunner(this);
1812 parallelQueryRunner_ = new QueryDependenciesParallelRunner(this);
1813
1814 // Apply saved parallel processor count
1815 parallelQueryRunner_->setParallelCount(
1816 AppConfig::instance().getParallelProcessorCount());
1817
1818 // Create make-ast runner
1819 makeAstRunner_ = new MakeAstRunner(this);
1820
1821 // Resolve Clang resource directory once and share with all runners
1822 const std::string clangResourceDir = acav::getClangResourceDir();
1823 if (!clangResourceDir.empty()) {
1824 QString dir = QString::fromStdString(clangResourceDir);
1825 queryRunner_->setClangResourceDir(dir);
1826 parallelQueryRunner_->setClangResourceDir(dir);
1827 makeAstRunner_->setClangResourceDir(dir);
1828 }
1829
1830 // Create initial AstContext
1831 astContext_ = std::make_unique<AstContext>();
1832 ++astVersion_;
1833 applyFontSize(AppConfig::instance().getFontSize());
1834
1835 // Create worker thread for AST extraction
1836 astWorkerThread_ = new QThread(this);
1837 astExtractorRunner_ = new AstExtractorRunner(astContext_.get(), fileManager_);
1838 astExtractorRunner_->setCommentExtractionEnabled(
1839 AppConfig::instance().getCommentExtractionEnabled());
1840
1841 // Create node cycle widget
1842 nodeCycleWidget_ = new NodeCycleWidget(this);
1843 astExtractorRunner_->moveToThread(astWorkerThread_);
1844 astWorkerThread_->start();
1845}
1846
1847void MainWindow::connectSignals() {
1848 // Menu actions
1849 connect(openAction_, &QAction::triggered, this,
1850 &MainWindow::onOpenCompilationDatabase);
1851 connect(exitAction_, &QAction::triggered, this, &MainWindow::onExit);
1852
1853 // Focus tracking for visual indicator (uses fast QPalette, not setStyleSheet)
1854 connect(qApp, &QApplication::focusChanged, this, &MainWindow::onFocusChanged);
1855
1856 // Keyboard shortcuts for switching focus between panes
1857 auto addFocusShortcut = [this](const QKeySequence &key, auto callback) {
1858 auto *action = new QAction(this);
1859 action->setShortcut(key);
1860 connect(action, &QAction::triggered, this, callback);
1861 addAction(action);
1862 };
1863
1864 addFocusShortcut(Qt::CTRL | Qt::Key_1, [this]() { tuView_->setFocus(); });
1865 addFocusShortcut(Qt::CTRL | Qt::Key_2, [this]() { sourceView_->setFocus(); });
1866 addFocusShortcut(Qt::CTRL | Qt::Key_3, [this]() { astView_->setFocus(); });
1867 addFocusShortcut(Qt::CTRL | Qt::Key_4,
1868 [this]() { declContextView_->focusSemanticTree(); });
1869 addFocusShortcut(Qt::CTRL | Qt::Key_5,
1870 [this]() { declContextView_->focusLexicalTree(); });
1871 addFocusShortcut(Qt::CTRL | Qt::Key_6, [this]() { logDock_->focusAllTab(); });
1872 addFocusShortcut(QKeySequence::Find, [this]() {
1873 if (focusedDock_ == tuDock_ && tuSearch_) {
1874 tuSearch_->setFocus();
1875 tuSearch_->selectAll();
1876 } else if (focusedDock_ == astDock_ && astSearchQuickInput_) {
1877 astSearchQuickInput_->setFocus();
1878 astSearchQuickInput_->selectAll();
1879 } else if (sourceSearchInput_) {
1880 sourceSearchInput_->setFocus();
1881 sourceSearchInput_->selectAll();
1882 }
1883 });
1884
1885 // Expand/Collapse shortcuts for tree views
1886 addFocusShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_E, [this]() {
1887 if (astView_->hasFocus()) {
1888 expandAllChildren(astView_);
1889 } else if (tuView_->hasFocus()) {
1890 onExpandAllTuChildren(); // Context-aware: directories vs source files
1891 }
1892 });
1893 addFocusShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_C, [this]() {
1894 if (astView_->hasFocus()) {
1895 collapseAllChildren(astView_);
1896 } else if (tuView_->hasFocus()) {
1897 onCollapseAllTuChildren(); // Context-aware: directories vs source files
1898 }
1899 });
1900
1901 // F5 to extract AST for selected file
1902 addFocusShortcut(Qt::Key_F5, [this]() {
1903 QModelIndex index = tuView_->currentIndex();
1904 if (index.isValid()) {
1905 onTranslationUnitSelected(index);
1906 }
1907 });
1908
1909 // Ctrl+I to inspect (view details of) the selected AST node
1910 addFocusShortcut(Qt::CTRL | Qt::Key_I, [this]() {
1911 QModelIndex index = astView_->currentIndex();
1912 if (!index.isValid()) {
1913 return;
1914 }
1915 auto *node = static_cast<AstViewNode *>(index.internalPointer());
1916 if (node) {
1917 onViewNodeDetails(node);
1918 }
1919 });
1920
1921 // View selections (keyboard/mouse current change to load source,
1922 // double-click to load AST)
1923 if (tuView_->selectionModel()) {
1924 connect(tuView_->selectionModel(), &QItemSelectionModel::currentChanged,
1925 this, [this](const QModelIndex &current, const QModelIndex &) {
1926 onTranslationUnitClicked(current);
1927 });
1928 }
1929 connect(tuView_, &QTreeView::doubleClicked, this,
1930 &MainWindow::onTranslationUnitSelected);
1931
1932 // Navigation signals
1933 connect(astView_->selectionModel(), &QItemSelectionModel::currentChanged,
1934 this, &MainWindow::onAstNodeSelected);
1935 // Update DeclContext panel when AST node selection changes
1936 connect(
1937 astView_->selectionModel(), &QItemSelectionModel::currentChanged, this,
1938 [this](const QModelIndex &current, const QModelIndex &) {
1939 if (isNavigatingFromDeclContext_) {
1940 return; // Don't update when navigation originated from DeclContext
1941 }
1942 if (!current.isValid()) {
1943 declContextView_->clear();
1944 return;
1945 }
1946 auto *node = static_cast<AstViewNode *>(current.internalPointer());
1947 declContextView_->setSelectedNode(node);
1948 });
1949 // Navigate AST when user clicks on a declaration context entry
1950 connect(declContextView_, &DeclContextView::contextNodeClicked, this,
1951 [this](AstViewNode *node) {
1952 if (!node) {
1953 return;
1954 }
1955 isNavigatingFromDeclContext_ = true;
1956 QModelIndex modelIndex = astModel_->selectNode(node);
1957 if (modelIndex.isValid()) {
1958 astView_->selectionModel()->setCurrentIndex(
1959 modelIndex, QItemSelectionModel::ClearAndSelect);
1960 astView_->scrollTo(modelIndex);
1961 }
1962 isNavigatingFromDeclContext_ = false;
1963 });
1964 astView_->setContextMenuPolicy(Qt::CustomContextMenu);
1965 connect(astView_, &QTreeView::customContextMenuRequested, this,
1966 &MainWindow::onAstContextMenuRequested);
1967 tuView_->setContextMenuPolicy(Qt::CustomContextMenu);
1968 connect(tuView_, &QTreeView::customContextMenuRequested, this,
1969 &MainWindow::onTuContextMenuRequested);
1970 connect(sourceView_, &SourceCodeView::sourcePositionClicked, this,
1971 &MainWindow::onSourcePositionClicked);
1972 connect(sourceView_, &SourceCodeView::sourceRangeSelected, this,
1973 &MainWindow::onSourceRangeSelected);
1974 connect(nodeCycleWidget_, &NodeCycleWidget::nodeSelected, this,
1975 &MainWindow::onCycleNodeSelected);
1976 connect(nodeCycleWidget_, &NodeCycleWidget::closed, this,
1977 &MainWindow::onCycleWidgetClosed);
1978
1979 // Query runner signals (sequential)
1980 connect(queryRunner_, &QueryDependenciesRunner::dependenciesReady, this,
1981 &MainWindow::onDependenciesReady);
1982 connect(queryRunner_, &QueryDependenciesRunner::dependenciesReadyWithErrors,
1983 this, &MainWindow::onDependenciesReadyWithErrors);
1984 connect(queryRunner_, &QueryDependenciesRunner::error, this,
1985 &MainWindow::onDependenciesError);
1986 connect(queryRunner_, &QueryDependenciesRunner::progress, this,
1987 &MainWindow::onDependenciesProgress);
1988 if (logDock_) {
1989 connect(queryRunner_, &QueryDependenciesRunner::logMessage, logDock_,
1990 &LogDock::enqueue);
1991 }
1992
1993 // Parallel query runner signals
1994 connect(parallelQueryRunner_,
1995 &QueryDependenciesParallelRunner::dependenciesReady, this,
1996 &MainWindow::onDependenciesReady);
1997 connect(parallelQueryRunner_,
1998 &QueryDependenciesParallelRunner::dependenciesReadyWithErrors, this,
1999 &MainWindow::onDependenciesReadyWithErrors);
2000 connect(parallelQueryRunner_, &QueryDependenciesParallelRunner::error, this,
2001 &MainWindow::onDependenciesError);
2002 connect(parallelQueryRunner_, &QueryDependenciesParallelRunner::progress,
2003 this, &MainWindow::onDependenciesProgress);
2004 if (logDock_) {
2005 connect(parallelQueryRunner_, &QueryDependenciesParallelRunner::logMessage,
2006 logDock_, &LogDock::enqueue);
2007 }
2008
2009 // Make-ast runner signals
2010 connect(makeAstRunner_, &MakeAstRunner::astReady, this,
2011 &MainWindow::onAstReady);
2012 connect(makeAstRunner_, &MakeAstRunner::error, this, &MainWindow::onAstError);
2013 connect(makeAstRunner_, &MakeAstRunner::progress, this,
2014 &MainWindow::onAstProgress);
2015 connect(makeAstRunner_, &MakeAstRunner::logMessage, this,
2016 &MainWindow::onMakeAstLogMessage);
2017 if (logDock_) {
2018 connect(makeAstRunner_, &MakeAstRunner::logMessage, logDock_,
2019 &LogDock::enqueue);
2020 }
2021
2022 // AST extractor signals (queued connections for cross-thread)
2023 connect(astExtractorRunner_, &AstExtractorRunner::finished, this,
2024 &MainWindow::onAstExtracted);
2025 connect(astExtractorRunner_, &AstExtractorRunner::error, this,
2026 &MainWindow::onAstError);
2027 connect(astExtractorRunner_, &AstExtractorRunner::progress, this,
2028 &MainWindow::onAstProgress);
2029 connect(astExtractorRunner_, &AstExtractorRunner::statsUpdated, this,
2030 &MainWindow::onAstStatsUpdated);
2031 connect(astExtractorRunner_, &AstExtractorRunner::started, this,
2032 &MainWindow::onAstProgress);
2033 if (logDock_) {
2034 connect(astExtractorRunner_, &AstExtractorRunner::logMessage, logDock_,
2035 &LogDock::enqueue, Qt::QueuedConnection);
2036 }
2037}
2038
2039void MainWindow::loadCompilationDatabase(const QString &compilationDatabasePath,
2040 const QString &projectRoot) {
2041 MemoryProfiler::checkpoint("Before loading compilation database");
2042
2043 // Normalize to absolute path to ensure it works regardless of current working
2044 // directory
2045 QFileInfo info(compilationDatabasePath);
2046 compilationDatabasePath_ = info.absoluteFilePath();
2047 const QString normalizedCompilationDatabasePath = compilationDatabasePath_;
2048
2049 // Store user-specified project root (may be empty - model will compute from
2050 // source files) If model's computation fails, it will use
2051 // compilationDatabasePath's directory as fallback
2052 if (projectRoot.isEmpty()) {
2053 projectRoot_ = QString(); // Let model compute from source files
2054 } else {
2055 projectRoot_ = QFileInfo(projectRoot).absoluteFilePath();
2056 }
2057
2058 // Get cache directory and output file path from config
2059 AppConfig &config = AppConfig::instance();
2060 QString outputFilePath =
2061 config.getDependenciesFilePath(normalizedCompilationDatabasePath);
2062 QString cacheDir =
2063 config.getCacheDirectory(normalizedCompilationDatabasePath);
2064
2065 MemoryProfiler::checkpoint("After config setup");
2066
2067 // Determine if we should use parallel processing
2068 std::string errorMsg;
2069 std::vector<std::string> sourceFiles =
2071 normalizedCompilationDatabasePath.toStdString(), errorMsg);
2072
2073 MemoryProfiler::checkpoint("After loading source files list");
2074
2075 auto dedupSources = [](const std::vector<std::string> &paths) {
2076 std::vector<std::string> unique;
2077 unique.reserve(paths.size());
2078 std::unordered_set<std::string> seen;
2079 for (const std::string &path : paths) {
2080 if (seen.insert(path).second) {
2081 unique.push_back(path);
2082 }
2083 }
2084 return unique;
2085 };
2086 sourceFiles = dedupSources(sourceFiles);
2087
2088 if (sourceFiles.empty()) {
2089 QMessageBox::critical(this, tr("Error"),
2090 tr("Failed to load source files: %1")
2091 .arg(QString::fromStdString(errorMsg)));
2092 return;
2093 }
2094
2095 bool useParallel =
2096 (static_cast<int>(sourceFiles.size()) >= kParallelThreshold);
2097
2098 if (useParallel) {
2099 logStatus(LogLevel::Info,
2100 QString("Starting parallel dependency analysis (%1 files)...")
2101 .arg(sourceFiles.size()),
2102 QStringLiteral("query-dependencies"));
2103 parallelQueryRunner_->run(normalizedCompilationDatabasePath,
2104 outputFilePath);
2105 } else {
2106 logStatus(LogLevel::Info,
2107 QString("Loading compilation database: %1\nCache directory: %2")
2108 .arg(normalizedCompilationDatabasePath)
2109 .arg(cacheDir),
2110 QStringLiteral("query-dependencies"));
2111 queryRunner_->run(normalizedCompilationDatabasePath, outputFilePath);
2112 }
2113}
2114
2115void MainWindow::onOpenCompilationDatabase() {
2116 if (isAstExportInProgress_) {
2117 logStatus(LogLevel::Info, tr("AST export in progress, please wait..."));
2118 return;
2119 }
2120
2121 OpenProjectDialog dialog(this);
2122 if (dialog.exec() != QDialog::Accepted) {
2123 return;
2124 }
2125
2126 QString dbPath = dialog.compilationDatabasePath();
2127 QString projectRoot = dialog.projectRootPath();
2128
2129 if (dbPath.isEmpty()) {
2130 return;
2131 }
2132
2133 loadCompilationDatabase(dbPath, projectRoot);
2134}
2135
2136void MainWindow::onExit() { close(); }
2137
2138void MainWindow::onTranslationUnitClicked(const QModelIndex &index) {
2139 if (!index.isValid()) {
2140 return;
2141 }
2142
2143 QString filePath = tuModel_->getSourceFilePathFromIndex(index);
2144 if (filePath.isEmpty()) {
2145 return;
2146 }
2147
2148 if (filePath == sourceView_->currentFilePath()) {
2149 return;
2150 }
2151
2152 logStatus(LogLevel::Info, QString("Loading file: %1").arg(filePath));
2153
2154 if (sourceView_->loadFile(filePath)) {
2155 // Register file with FileManager and set FileID in SourceCodeView
2156 FileID fileId = fileManager_.registerFile(filePath.toStdString());
2157 sourceView_->setCurrentFileId(fileId);
2158 updateSourceSubtitle(filePath);
2159 // Don't call highlightTuFile here - user's click already set the selection
2160 logStatus(LogLevel::Info, QString("Loaded: %1").arg(filePath));
2161
2162 // Clear AST Tree and Declaration Context to maintain UI consistency.
2163 // User should not see AST from file A while viewing source code from file
2164 // B.
2165 bool shouldClearAst = false;
2166 if (isSourceFile(filePath)) {
2167 // Clicking a different source file means a different TU
2168 shouldClearAst = (filePath != currentSourceFilePath_);
2169 } else if (astContext_) {
2170 // For header files, check if it's part of the current AST
2171 const auto &index = astContext_->getLocationIndex();
2172 shouldClearAst = !index.hasFile(fileId);
2173 }
2174
2175 if (shouldClearAst) {
2176 astModel_->clear();
2177 declContextView_->clear();
2178 astHasCompilationErrors_ = false;
2179 setAstCompilationWarningVisible(false);
2180 }
2181 } else {
2182 sourceView_->setCurrentFileId(FileManager::InvalidFileID);
2183 logStatus(LogLevel::Error, QString("Failed to load: %1").arg(filePath));
2184 return;
2185 }
2186}
2187
2188void MainWindow::onTranslationUnitSelected(const QModelIndex &index) {
2189 if (!index.isValid()) {
2190 return;
2191 }
2192
2193 QString filePath = tuModel_->getSourceFilePathFromIndex(index);
2194 if (filePath.isEmpty()) {
2195 return;
2196 }
2197
2198 MemoryProfiler::checkpoint("File double-clicked - before checks");
2199
2200 // Check if this is already the current file with AST loaded
2201 if (filePath == currentSourceFilePath_ && astModel_->hasNodes()) {
2202 logStatus(LogLevel::Info,
2203 QString("AST already loaded for: %1").arg(filePath));
2204 return;
2205 }
2206
2207 // Check if source file (not header)
2208 if (!isSourceFile(filePath)) {
2209 logStatus(LogLevel::Warning, tr("Cannot load AST for header files"));
2210 // Still load the source code for viewing
2211 if (sourceView_->loadFile(filePath)) {
2212 FileID fileId = fileManager_.registerFile(filePath.toStdString());
2213 sourceView_->setCurrentFileId(fileId);
2214 updateSourceSubtitle(filePath);
2215 highlightTuFile(fileId);
2216 }
2217 return;
2218 }
2219
2220 // Check if AST extraction is already in progress
2221 if (isAstExtractionInProgress_) {
2222 // Simply show message and ignore the request
2223 QFileInfo currentInfo(pendingSourceFilePath_);
2224 logStatus(
2225 LogLevel::Info,
2226 QString("AST extraction already in progress for %1. Please wait...")
2227 .arg(currentInfo.fileName()));
2228 return;
2229 }
2230
2231 if (isAstExportInProgress_) {
2232 logStatus(LogLevel::Info, tr("AST export in progress, please wait..."));
2233 return;
2234 }
2235
2236 MemoryProfiler::checkpoint("Before clearing old AST");
2237
2238 // Clear old AST and create new context for new TU
2239 astModel_->clear();
2240 astHasCompilationErrors_ = false;
2241 setAstCompilationWarningVisible(false);
2242
2243 MemoryProfiler::checkpoint("After clearing old AST model");
2244
2245 clearHistory();
2246
2247 MemoryProfiler::checkpoint("After clearHistory()");
2248
2249 // Safely clean up the old extractor before destroying context
2250 // The extractor lives on the worker thread, so we must handle it carefully
2251 if (astExtractorRunner_) {
2252 // Disconnect all signals to prevent callbacks during destruction
2253 astExtractorRunner_->disconnect();
2254 // Move back to main thread for safe deletion
2255 astExtractorRunner_->moveToThread(QThread::currentThread());
2256 delete astExtractorRunner_;
2257 astExtractorRunner_ = nullptr;
2258 }
2259
2260 astContext_.reset(); // Destroys old context (cleans up old TU nodes)
2261
2262 MemoryProfiler::checkpoint("After destroying old AST context");
2263
2264 astContext_ = std::make_unique<AstContext>();
2265 ++astVersion_;
2266
2267 MemoryProfiler::checkpoint("After creating new AST context");
2268
2269 // Create new extractor with new context
2270 astExtractorRunner_ = new AstExtractorRunner(astContext_.get(), fileManager_);
2271 astExtractorRunner_->setCommentExtractionEnabled(
2272 AppConfig::instance().getCommentExtractionEnabled());
2273 astExtractorRunner_->moveToThread(astWorkerThread_);
2274
2275 // Reconnect signals
2276 connect(astExtractorRunner_, &AstExtractorRunner::finished, this,
2277 &MainWindow::onAstExtracted);
2278 connect(astExtractorRunner_, &AstExtractorRunner::error, this,
2279 &MainWindow::onAstError);
2280 connect(astExtractorRunner_, &AstExtractorRunner::progress, this,
2281 &MainWindow::onAstProgress);
2282 connect(astExtractorRunner_, &AstExtractorRunner::statsUpdated, this,
2283 &MainWindow::onAstStatsUpdated);
2284 connect(astExtractorRunner_, &AstExtractorRunner::started, this,
2285 &MainWindow::onAstProgress);
2286
2287 logStatus(LogLevel::Info, QString("Loading file: %1").arg(filePath));
2288
2289 if (sourceView_->loadFile(filePath)) {
2290 // Register file with FileManager and set FileID in SourceCodeView
2291 FileID fileId = fileManager_.registerFile(filePath.toStdString());
2292 sourceView_->setCurrentFileId(fileId);
2293 updateSourceSubtitle(filePath);
2294 logStatus(LogLevel::Info, QString("Loaded: %1").arg(filePath));
2295 } else {
2296 sourceView_->setCurrentFileId(FileManager::InvalidFileID);
2297 logStatus(LogLevel::Error, QString("Failed to load: %1").arg(filePath));
2298 return;
2299 }
2300
2301 MemoryProfiler::checkpoint("After loading source file to view");
2302
2303 // Get .ast cache file path
2304 AppConfig &config = AppConfig::instance();
2305 QString astFilePath =
2306 config.getAstFilePath(compilationDatabasePath_, filePath);
2307
2308 QFileInfo astFileInfo(astFilePath);
2309
2310 if (!astFileInfo.exists()) {
2311 // Generate .ast file using make-ast
2312 currentSourceFilePath_ = filePath;
2313 pendingSourceFilePath_ = filePath;
2314 isAstExtractionInProgress_ = true;
2315 astHasCompilationErrors_ = false;
2316 QFileInfo sourceInfo(filePath);
2317 logStatus(LogLevel::Info,
2318 "Generating AST for " + sourceInfo.fileName() + "...");
2319 onTimingMessage(QString("AST input files: %1 (source + headers)")
2320 .arg(getFileListForSource(filePath).size()));
2321 MemoryProfiler::checkpoint("Before make-ast generation");
2322 makeAstRunner_->run(compilationDatabasePath_, filePath, astFilePath);
2323 } else {
2324 currentSourceFilePath_ = filePath;
2325 pendingSourceFilePath_ = filePath;
2326 isAstExtractionInProgress_ = true;
2327 astHasCompilationErrors_ = loadAstCompilationErrorState(astFilePath);
2328 onTimingMessage(QString("AST input files: %1 (source + headers)")
2329 .arg(getFileListForSource(filePath).size()));
2330 MemoryProfiler::checkpoint("Before AST extraction from cache");
2331 // .ast exists, extract directly
2332 extractAst(astFilePath, filePath);
2333 }
2334}
2335
2336void MainWindow::onDependenciesReady(const QJsonObject &dependencies) {
2337 tuModel_->populateFromDependencies(dependencies, projectRoot_,
2338 compilationDatabasePath_);
2339 expandFileExplorerTopLevel();
2340
2341 // Get file count from statistics
2342 QJsonObject stats = dependencies["statistics"].toObject();
2343 int fileCount = stats["successCount"].toInt();
2344 int totalHeaders = stats["totalHeaderCount"].toInt();
2345
2346 logStatus(LogLevel::Info,
2347 QString("Loaded %1 translation units").arg(fileCount),
2348 QStringLiteral("query-dependencies"));
2349 onTimingMessage(QString("Dependencies summary: %1 sources, %2 headers")
2350 .arg(fileCount)
2351 .arg(totalHeaders));
2352}
2353
2354void MainWindow::onDependenciesError(const QString &errorMessage) {
2355 QMessageBox::critical(this, tr("Error"), errorMessage);
2356 logStatus(LogLevel::Error, tr("Error loading dependencies"),
2357 QStringLiteral("query-dependencies"));
2358}
2359
2360void MainWindow::onDependenciesProgress(const QString &message) {
2361 logStatus(LogLevel::Info, message, QStringLiteral("query-dependencies"));
2362}
2363
2364void MainWindow::onAstProgress(const QString &message) {
2365 logStatus(LogLevel::Info, message, QStringLiteral("ast-extractor"));
2366}
2367
2368void MainWindow::onAstStatsUpdated(const AstExtractionStats &stats) {
2369 logStatus(LogLevel::Info,
2370 QString("Comments found: %1").arg(stats.commentCount),
2371 QStringLiteral("ast-extractor"));
2372}
2373
2374void MainWindow::onDependenciesReadyWithErrors(
2375 const QJsonObject &dependencies, const QStringList &errorMessages) {
2376 // Load successful dependencies normally
2377 tuModel_->populateFromDependencies(dependencies, projectRoot_,
2378 compilationDatabasePath_);
2379 expandFileExplorerTopLevel();
2380
2381 QJsonObject stats = dependencies["statistics"].toObject();
2382 int successCount = stats["successCount"].toInt();
2383 int failureCount = stats["failureCount"].toInt();
2384 int totalHeaders = stats["totalHeaderCount"].toInt();
2385
2386 logStatus(LogLevel::Warning,
2387 QString("Loaded %1 translation units (%2 failed)")
2388 .arg(successCount)
2389 .arg(failureCount),
2390 QStringLiteral("query-dependencies"));
2391 for (const QString &errorMessage : errorMessages) {
2392 logStatus(LogLevel::Error, errorMessage,
2393 QStringLiteral("query-dependencies"));
2394 }
2395 onTimingMessage(
2396 QString("Dependencies summary: %1 sources loaded, %2 failed, %3 headers")
2397 .arg(successCount)
2398 .arg(failureCount)
2399 .arg(totalHeaders));
2400}
2401
2402void MainWindow::logStatus(LogLevel level, const QString &message,
2403 const QString &source) {
2404 const QString trimmed = message.trimmed();
2405 if (trimmed.isEmpty()) {
2406 return;
2407 }
2408
2409 LogEntry entry;
2410 entry.level = level;
2411 entry.source = source.isEmpty() ? QStringLiteral("acav") : source;
2412 entry.message = trimmed;
2413 entry.timestamp = QDateTime::currentDateTime();
2414
2415 if (logDock_) {
2416 QMetaObject::invokeMethod(logDock_, "enqueue", Qt::QueuedConnection,
2417 Q_ARG(LogEntry, entry));
2418 }
2419}
2420
2421void MainWindow::onAstReady(const QString &astFilePath) {
2422 MemoryProfiler::checkpoint("After make-ast generation complete");
2423 persistAstCompilationErrorState(astFilePath, astHasCompilationErrors_);
2424 // make-ast finished, now extract
2425 extractAst(astFilePath, currentSourceFilePath_);
2426}
2427
2428void MainWindow::onAstExtracted(AstViewNode *root) {
2429 MemoryProfiler::checkpoint("AST extraction complete - before rendering");
2430
2431 // Clear stale AST search state
2432 clearAstSearchState();
2433 if (astSearchInput_) {
2434 astSearchInput_->clear();
2435 }
2436
2437 // Clear extraction in progress flag
2438 isAstExtractionInProgress_ = false;
2439
2440 // Reload source view to ensure synchronization with AST
2441 // Handles case where user clicked a different file during processing
2442 if (!currentSourceFilePath_.isEmpty()) {
2443 // Check if source view is showing a different file
2444 if (sourceView_->currentFilePath() != currentSourceFilePath_) {
2445 // Load the file that matches the extracted AST
2446 if (sourceView_->loadFile(currentSourceFilePath_)) {
2447 // Register file with FileManager and set FileID
2448 FileID fileId =
2449 fileManager_.registerFile(currentSourceFilePath_.toStdString());
2450 sourceView_->setCurrentFileId(fileId);
2451 updateSourceSubtitle(currentSourceFilePath_);
2452 } else {
2453 // File load failed (deleted, moved, permission denied, etc.)
2454 sourceView_->setCurrentFileId(FileManager::InvalidFileID);
2455 logStatus(LogLevel::Warning, QString("Failed to reload source file: %1")
2456 .arg(currentSourceFilePath_));
2457 // Continue with AST rendering - AST is still useful without source view
2458 }
2459 }
2460
2461 // Highlight the current file in the Translation Unit tree view
2462 // This provides visual feedback showing which file is currently active
2463 // Use FileID for consistent file identification throughout the application
2464 FileID currentFileId = sourceView_->currentFileId();
2465 if (currentFileId != FileManager::InvalidFileID) {
2466 QModelIndex tuIndex = tuModel_->findIndexByFileId(currentFileId);
2467 if (tuIndex.isValid()) {
2468 tuView_->setCurrentIndex(tuIndex);
2469 tuView_->scrollTo(tuIndex);
2470 }
2471 }
2472 }
2473
2474 auto renderStart = std::chrono::steady_clock::now();
2475
2476 MemoryProfiler::checkpoint("Before setting root node to model");
2477
2478 std::size_t nodeCount = 0;
2479 if (astContext_) {
2480 nodeCount = astContext_->getAstViewNodeCount();
2481 astModel_->setTotalNodeCount(nodeCount);
2482 }
2483
2484 // Update model (on main thread via queued signal)
2485 astModel_->setRootNode(root); // Takes ownership
2486 updateAstSubtitle(currentSourceFilePath_);
2487
2488 MemoryProfiler::checkpoint("After setting root node to model");
2489
2490 auto renderEnd = std::chrono::steady_clock::now();
2491 std::chrono::duration<double> renderElapsed = renderEnd - renderStart;
2492 onTimingMessage(QString("render AST: %1s")
2493 .arg(QString::number(renderElapsed.count(), 'f', 2)));
2494 onTimingMessage(QString("AST nodes loaded: %1").arg(nodeCount));
2495
2496 MemoryProfiler::checkpoint(
2497 QString("AST rendering complete (%1 nodes)").arg(nodeCount));
2498
2499 const bool showCompilationWarning = astHasCompilationErrors_;
2500 setAstCompilationWarningVisible(showCompilationWarning);
2501 astHasCompilationErrors_ = false;
2502
2503 logStatus(LogLevel::Info, tr("AST loaded (%1 nodes)").arg(nodeCount));
2504}
2505
2506void MainWindow::onAstError(const QString &errorMessage) {
2507 // Clear extraction in progress flag
2508 isAstExtractionInProgress_ = false;
2509 astHasCompilationErrors_ = false;
2510 setAstCompilationWarningVisible(false);
2511
2512 QString targetAstPath = lastAstFilePath_;
2513 if (targetAstPath.isEmpty() && !compilationDatabasePath_.isEmpty() &&
2514 !currentSourceFilePath_.isEmpty()) {
2515 targetAstPath = AppConfig::instance().getAstFilePath(
2516 compilationDatabasePath_, currentSourceFilePath_);
2517 lastAstFilePath_ = targetAstPath;
2518 }
2519
2520 if (targetAstPath.isEmpty() || currentSourceFilePath_.isEmpty()) {
2521 logStatus(LogLevel::Error, "Error: " + errorMessage);
2522 QMessageBox::critical(this, "AST Error",
2523 "Failed to generate or load AST:\n\n" + errorMessage);
2524 return;
2525 }
2526
2527 const bool cacheLoadFailed =
2528 errorMessage.contains("Failed to load AST from file",
2529 Qt::CaseInsensitive) ||
2530 errorMessage.contains("Failed to load AST", Qt::CaseInsensitive);
2531 if (!cacheLoadFailed) {
2532 logStatus(LogLevel::Error, "Error: " + errorMessage);
2533 QMessageBox::critical(this, "AST Error",
2534 "Failed to generate or load AST:\n\n" + errorMessage);
2535 return;
2536 }
2537
2538 logStatus(LogLevel::Warning,
2539 tr("Cached AST load failed; regenerating automatically."));
2540 logStatus(LogLevel::Info, errorMessage);
2541
2542 if (!deleteCachedAst(targetAstPath)) {
2543 logStatus(LogLevel::Error,
2544 tr("Failed to delete cached AST file: %1").arg(targetAstPath));
2545 QMessageBox::critical(this, "AST Error",
2546 "Failed to delete cached AST file:\n\n" +
2547 targetAstPath);
2548 return;
2549 }
2550
2551 logStatus(LogLevel::Warning, tr("Regenerating AST after load failure..."));
2552 astHasCompilationErrors_ = false;
2553 makeAstRunner_->run(compilationDatabasePath_, currentSourceFilePath_,
2554 targetAstPath);
2555}
2556
2557void MainWindow::onMakeAstLogMessage(const LogEntry &entry) {
2558 if (entry.source == QStringLiteral("make-ast") &&
2559 entry.level == LogLevel::Error) {
2560 astHasCompilationErrors_ = true;
2561 }
2562}
2563
2564void MainWindow::onAstContextMenuRequested(const QPoint &pos) {
2565 QModelIndex index = astView_->indexAt(pos);
2566 if (!index.isValid()) {
2567 return;
2568 }
2569
2570 auto *node = static_cast<AstViewNode *>(index.internalPointer());
2571 if (!node) {
2572 return;
2573 }
2574
2575 // Root node is the translation unit
2576 const bool isTranslationUnit = (node->getParent() == nullptr);
2577
2578 ::QMenu menu(astView_);
2579
2580 SourceRange macroRange(SourceLocation(FileManager::InvalidFileID, 0, 0),
2581 SourceLocation(FileManager::InvalidFileID, 0, 0));
2582 const bool hasMacroRange = getMacroSpellingRange(node, &macroRange);
2583 if (hasMacroRange) {
2584 QAction *macroAction = menu.addAction(tr("Go to Macro Definition"));
2585 connect(macroAction, &QAction::triggered, this, [this, node, macroRange]() {
2586 navigateToRange(macroRange, node, false);
2587 });
2588 menu.addSeparator();
2589 }
2590
2591 // Expand/Collapse actions
2592 QAction *expandAllAction = menu.addAction(tr("Expand All"));
2593 QAction *collapseAllAction = menu.addAction(tr("Collapse All"));
2594 connect(expandAllAction, &QAction::triggered, this,
2595 &MainWindow::onExpandAllAstChildren);
2596 connect(collapseAllAction, &QAction::triggered, this,
2597 &MainWindow::onCollapseAllAstChildren);
2598
2599 // View Details action
2600 QAction *viewDetailsAction = menu.addAction(tr("View Details..."));
2601 viewDetailsAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_I));
2602 connect(viewDetailsAction, &QAction::triggered, this,
2603 [this, node]() { onViewNodeDetails(node); });
2604
2605 // Export action - disabled for translation unit (too large)
2606 menu.addSeparator();
2607 QAction *exportAction = menu.addAction(tr("Export Subtree to JSON..."));
2608 exportAction->setEnabled(!isTranslationUnit && astModel_ &&
2609 astModel_->hasNodes());
2610 connect(exportAction, &QAction::triggered, this,
2611 [this, node]() { onExportAst(node); });
2612
2613 menu.exec(astView_->viewport()->mapToGlobal(pos));
2614}
2615
2616void MainWindow::onViewNodeDetails(AstViewNode *node) {
2617 if (!node) {
2618 return;
2619 }
2620
2621 const AcavJson &props = node->getProperties();
2622 QString kind = tr("Node");
2623 QString name;
2624
2625 if (props.contains("kind") && props.at("kind").is_string()) {
2626 kind = QString::fromStdString(props.at("kind").get<InternedString>().str());
2627 }
2628 if (props.contains("name") && props.at("name").is_string()) {
2629 name = QString::fromStdString(props.at("name").get<InternedString>().str());
2630 }
2631
2632 QString title = tr("Node Details - %1").arg(kind);
2633 if (!name.isEmpty()) {
2634 title += QStringLiteral(" '%1'").arg(name);
2635 }
2636
2637 AcavJson propertiesCopy = props;
2638
2639 // Add source range information with file paths
2640 const SourceRange &range = node->getSourceRange();
2641 AcavJson sourceRangeJson = AcavJson::object();
2642
2643 auto locationToJson = [this](const SourceLocation &loc) {
2644 AcavJson obj = AcavJson::object();
2645 obj["fileId"] = static_cast<uint64_t>(loc.fileID());
2646 obj["line"] = static_cast<uint64_t>(loc.line());
2647 obj["column"] = static_cast<uint64_t>(loc.column());
2648 std::string_view filePath = fileManager_.getFilePath(loc.fileID());
2649 if (!filePath.empty()) {
2650 obj["filePath"] = InternedString(std::string(filePath));
2651 }
2652 return obj;
2653 };
2654
2655 sourceRangeJson["begin"] = locationToJson(range.begin());
2656 sourceRangeJson["end"] = locationToJson(range.end());
2657 propertiesCopy["sourceRange"] = std::move(sourceRangeJson);
2658
2659 auto *dialog = new NodeDetailsDialog(std::move(propertiesCopy), title, this);
2660 dialog->setAttribute(Qt::WA_DeleteOnClose);
2661 dialog->show();
2662 dialog->raise();
2663 dialog->activateWindow();
2664}
2665
2666void MainWindow::onExportAst(AstViewNode *node) {
2667 if (!node) {
2668 return;
2669 }
2670 if (isAstExtractionInProgress_) {
2671 logStatus(LogLevel::Info, tr("AST extraction in progress, please wait..."));
2672 return;
2673 }
2674 if (isAstExportInProgress_) {
2675 logStatus(LogLevel::Info, tr("AST export already in progress..."));
2676 return;
2677 }
2678
2679 QString defaultDir;
2680 if (!currentSourceFilePath_.isEmpty()) {
2681 QFileInfo info(currentSourceFilePath_);
2682 defaultDir = info.absolutePath();
2683 } else {
2684 defaultDir = QDir::homePath();
2685 }
2686
2687 QString suggested =
2688 defaultDir + QDir::separator() + buildDefaultExportFileName(node);
2689
2690 QString targetPath =
2691 QFileDialog::getSaveFileName(this, tr("Export AST Subtree"), suggested,
2692 tr("JSON Files (*.json);;All Files (*)"));
2693 if (targetPath.isEmpty()) {
2694 return;
2695 }
2696
2697 auto confirm = QMessageBox::question(
2698 this, tr("Export AST"),
2699 tr("Exporting this subtree can take some time.\n\nDo you want to "
2700 "continue?"),
2701 QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
2702 if (confirm != QMessageBox::Yes) {
2703 return;
2704 }
2705
2706 auto *progress = new QProgressDialog(this);
2707 progress->setAttribute(Qt::WA_DeleteOnClose);
2708 progress->setWindowTitle(tr("Exporting AST"));
2709 progress->setLabelText(tr("Exporting AST subtree to JSON in the background.\n"
2710 "You can close this window and continue working.\n"
2711 "You will be notified when the export finishes."));
2712 progress->setCancelButtonText(tr("Close"));
2713 progress->setRange(0, 0); // Busy indicator
2714 progress->setWindowModality(Qt::NonModal);
2715 progress->show();
2716 connect(progress, &QProgressDialog::canceled, progress,
2717 &QProgressDialog::close);
2718
2719 isAstExportInProgress_ = true;
2720 QPointer<MainWindow> self(this);
2721 QPointer<QProgressDialog> progressPtr(progress);
2722 FileManager *fileManager = &fileManager_;
2723 AstViewNode *exportRoot = node;
2724 QString exportPath = targetPath;
2725
2726 auto *thread = new QThread(this);
2727 astExportThread_ = thread;
2728 auto *worker = new QObject();
2729 worker->moveToThread(thread);
2730
2731 connect(
2732 thread, &QThread::started, worker,
2733 [self, progressPtr, exportRoot, exportPath, fileManager, thread]() {
2734 QString errorMessage;
2735 try {
2736 AcavJson json = buildAstJsonTree(exportRoot, *fileManager);
2737 InternedString serialized = json.dump(2);
2738 const std::string &data = serialized.str();
2739
2740 QFile outFile(exportPath);
2741 if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
2742 errorMessage = QObject::tr("Unable to open file for writing:\n%1")
2743 .arg(exportPath);
2744 } else if (outFile.write(data.c_str(),
2745 static_cast<qsizetype>(data.size())) == -1) {
2746 errorMessage = QObject::tr("Failed to write data to file:\n%1")
2747 .arg(exportPath);
2748 } else {
2749 outFile.close();
2750 }
2751 } catch (const std::exception &ex) {
2752 errorMessage = QObject::tr("Failed to serialize AST subtree:\n%1")
2753 .arg(ex.what());
2754 }
2755
2756 if (self) {
2757 QMetaObject::invokeMethod(
2758 self,
2759 [self, progressPtr, exportPath, errorMessage]() {
2760 if (!self) {
2761 return;
2762 }
2763 self->isAstExportInProgress_ = false;
2764 if (progressPtr) {
2765 progressPtr->close();
2766 }
2767 auto *notice = new QMessageBox(self);
2768 notice->setAttribute(Qt::WA_DeleteOnClose);
2769 notice->setStandardButtons(QMessageBox::Ok);
2770 notice->setWindowModality(Qt::NonModal);
2771 if (!errorMessage.isEmpty()) {
2772 notice->setIcon(QMessageBox::Warning);
2773 notice->setWindowTitle(QObject::tr("Export Failed"));
2774 notice->setText(errorMessage);
2775 } else {
2776 notice->setIcon(QMessageBox::Information);
2777 notice->setWindowTitle(QObject::tr("Export Complete"));
2778 notice->setText(QObject::tr("Exported AST subtree to:\n%1")
2779 .arg(exportPath));
2780 self->logStatus(LogLevel::Info,
2781 QObject::tr("Exported AST subtree to %1")
2782 .arg(exportPath));
2783 }
2784 notice->show();
2785 },
2786 Qt::QueuedConnection);
2787 }
2788
2789 thread->quit();
2790 });
2791
2792 connect(thread, &QThread::finished, worker, &QObject::deleteLater);
2793 connect(thread, &QThread::finished, thread, &QObject::deleteLater);
2794 connect(thread, &QThread::finished, this,
2795 [this]() { astExportThread_ = nullptr; });
2796 thread->start();
2797}
2798
2799QString MainWindow::buildDefaultExportFileName(AstViewNode *node) const {
2800 if (!node) {
2801 return QStringLiteral("ast_subtree.json");
2802 }
2803
2804 const AcavJson &props = node->getProperties();
2805 auto getStr = [&](const char *key) -> QString {
2806 if (props.contains(key) && props.at(key).is_string()) {
2807 return QString::fromStdString(props.at(key).get<InternedString>().str());
2808 }
2809 return {};
2810 };
2811
2812 QString kind = getStr("kind");
2813
2814 // Try multiple property names for the node's name
2815 QString name;
2816 for (const char *key : {"name", "declName", "memberName"}) {
2817 name = getStr(key);
2818 if (!name.isEmpty()) {
2819 break;
2820 }
2821 }
2822
2823 QString base = name.isEmpty() ? kind : QString("%1_%2").arg(kind, name);
2824 if (base.isEmpty()) {
2825 base = QStringLiteral("ast_subtree");
2826 }
2827
2828 // Sanitize filename: keep alphanumeric, underscore, hyphen
2829 QString sanitized;
2830 sanitized.reserve(base.size());
2831 for (QChar ch : base) {
2832 if (ch.isLetterOrNumber() || ch == '_' || ch == '-') {
2833 sanitized.append(ch);
2834 } else if (!sanitized.endsWith('_')) {
2835 sanitized.append('_');
2836 }
2837 }
2838
2839 if (sanitized.isEmpty()) {
2840 return QStringLiteral("ast_subtree.json");
2841 }
2842
2843 return sanitized + ".json";
2844}
2845
2846void MainWindow::extractAst(const QString &astFilePath,
2847 const QString &sourceFilePath) {
2848 MemoryProfiler::checkpoint("Starting extractAst - queuing to worker");
2849
2850 // Get file list from TU model
2851 QStringList fileList = getFileListForSource(sourceFilePath);
2852 lastAstFilePath_ = astFilePath;
2853 currentSourceFilePath_ = sourceFilePath;
2854
2855 // Extract on worker thread
2856 // Pass compilation database path for C++20 module support
2857 QString compDbPath = compilationDatabasePath_;
2858 QMetaObject::invokeMethod(
2859 astExtractorRunner_,
2860 [this, astFilePath, fileList, compDbPath]() {
2861 astExtractorRunner_->run(astFilePath, fileList, compDbPath);
2862 },
2863 Qt::QueuedConnection);
2864}
2865
2866QStringList
2867MainWindow::getFileListForSource(const QString &sourceFilePath) const {
2868 QStringList fileList;
2869 fileList.append(sourceFilePath); // Source file itself
2870
2871 // Get all included headers from TU model
2872 QStringList headers = tuModel_->getIncludedHeadersForSource(sourceFilePath);
2873 fileList.append(headers);
2874
2875 return fileList;
2876}
2877
2878bool MainWindow::isSourceFile(const QString &filePath) const {
2879 static const QStringList sourceExtensions = {// C/C++
2880 ".cpp", ".cc", ".cxx", ".c",
2881 // Objective-C/C++
2882 ".m", ".mm"};
2883 for (const QString &ext : sourceExtensions) {
2884 if (filePath.endsWith(ext, Qt::CaseInsensitive)) {
2885 return true;
2886 }
2887 }
2888 return false;
2889}
2890
2891bool MainWindow::validateSourceLookup(FileID fileId) {
2892 if (isAstExtractionInProgress_) {
2893 QFileInfo currentInfo(pendingSourceFilePath_);
2894 logStatus(LogLevel::Info,
2895 tr("AST extraction in progress for %1. Please wait...")
2896 .arg(currentInfo.fileName()));
2897 return false;
2898 }
2899 if (!astModel_ || !astModel_->hasNodes()) {
2900 logStatus(LogLevel::Info,
2901 tr("No AST available (code not yet compiled). Double-click the "
2902 "file in File Explorer to generate an AST."));
2903 return false;
2904 }
2905 if (!currentSourceFilePath_.isEmpty() && sourceView_ &&
2906 isSourceFile(sourceView_->currentFilePath())) {
2907 FileID currentAstFileId = FileManager::InvalidFileID;
2908 if (auto existing =
2909 fileManager_.tryGetFileId(currentSourceFilePath_.toStdString())) {
2910 currentAstFileId = *existing;
2911 }
2912 if (currentAstFileId != FileManager::InvalidFileID &&
2913 currentAstFileId != fileId) {
2914 logStatus(LogLevel::Info,
2915 tr("No AST available for this file yet. Double-click it in "
2916 "File Explorer to generate an AST."));
2917 return false;
2918 }
2919 }
2920 return true;
2921}
2922
2923void MainWindow::logNoNodeFound(FileID fileId, const QString &fallbackMessage) {
2924 const auto &index = astContext_->getLocationIndex();
2925 if (!index.hasFile(fileId)) {
2926 logStatus(LogLevel::Info,
2927 tr("No AST data for this file in the current translation unit. "
2928 "If this is a header, it may not be included. Double-click a "
2929 "source file in File Explorer to load its AST."));
2930 } else {
2931 logStatus(LogLevel::Info, fallbackMessage);
2932 }
2933}
2934
2935bool MainWindow::deleteCachedAst(const QString &astFilePath) {
2936 QFile astFile(astFilePath);
2937 if (astFile.exists() && !astFile.remove()) {
2938 return false;
2939 }
2940
2941 const QString statusPath = astCacheStatusFilePath(astFilePath);
2942 QFile statusFile(statusPath);
2943 if (statusFile.exists() && !statusFile.remove()) {
2944 logStatus(LogLevel::Warning,
2945 tr("Failed to delete AST cache status file: %1").arg(statusPath));
2946 }
2947 return true;
2948}
2949
2950QString MainWindow::astCacheStatusFilePath(const QString &astFilePath) const {
2951 return astFilePath + ".status";
2952}
2953
2954void MainWindow::persistAstCompilationErrorState(const QString &astFilePath,
2955 bool hasCompilationErrors) {
2956 if (astFilePath.isEmpty()) {
2957 return;
2958 }
2959
2960 const QString statusPath = astCacheStatusFilePath(astFilePath);
2961 QFile statusFile(statusPath);
2962 if (!statusFile.open(QIODevice::WriteOnly | QIODevice::Truncate |
2963 QIODevice::Text)) {
2964 logStatus(LogLevel::Warning,
2965 tr("Failed to write AST cache status file: %1").arg(statusPath));
2966 return;
2967 }
2968
2969 const QByteArray payload = hasCompilationErrors ? QByteArrayLiteral("1\n")
2970 : QByteArrayLiteral("0\n");
2971 if (statusFile.write(payload) != payload.size()) {
2972 logStatus(LogLevel::Warning,
2973 tr("Failed to persist AST cache status: %1").arg(statusPath));
2974 }
2975}
2976
2977bool MainWindow::loadAstCompilationErrorState(const QString &astFilePath) const {
2978 if (astFilePath.isEmpty()) {
2979 return false;
2980 }
2981
2982 QFile statusFile(astCacheStatusFilePath(astFilePath));
2983 if (!statusFile.exists() ||
2984 !statusFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
2985 return false;
2986 }
2987
2988 const QByteArray value = statusFile.readAll().trimmed().toLower();
2989 return value == "1" || value == "true";
2990}
2991
2992void MainWindow::clearHistory() {
2993 history_.clear();
2994 historyCursor_ = 0;
2995 updateNavActions();
2996}
2997
2998void MainWindow::recordHistory(FileID fileId, unsigned line, unsigned column,
2999 AstViewNode *node) {
3000 if (suppressHistory_ || fileId == FileManager::InvalidFileID) {
3001 return;
3002 }
3003
3004 NavEntry entry{fileId, line, column, node, astVersion_};
3005 if (!history_.empty()) {
3006 const NavEntry &current = history_[historyCursor_];
3007 if (current.fileId == entry.fileId && current.line == entry.line &&
3008 current.column == entry.column && current.node == entry.node &&
3009 current.astVersion == entry.astVersion) {
3010 return; // No-op
3011 }
3012 }
3013
3014 // Drop any forward history
3015 if (historyCursor_ + 1 < history_.size()) {
3016 history_.erase(history_.begin() + static_cast<long>(historyCursor_) + 1,
3017 history_.end());
3018 }
3019
3020 history_.push_back(entry);
3021 historyCursor_ = history_.size() - 1;
3022
3023 static constexpr std::size_t kMaxHistory = 500;
3024 if (history_.size() > kMaxHistory) {
3025 std::size_t toRemove = history_.size() - kMaxHistory;
3026 history_.erase(history_.begin(),
3027 history_.begin() + static_cast<long>(toRemove));
3028 historyCursor_ = history_.size() - 1;
3029 }
3030
3031 updateNavActions();
3032}
3033
3034void MainWindow::navigateHistory(int delta) {
3035 if (history_.empty()) {
3036 return;
3037 }
3038 int newIndex = static_cast<int>(historyCursor_) + delta;
3039 if (newIndex < 0 || newIndex >= static_cast<int>(history_.size())) {
3040 return;
3041 }
3042 historyCursor_ = static_cast<std::size_t>(newIndex);
3043 applyEntry(history_[historyCursor_]);
3044 updateNavActions();
3045}
3046
3047void MainWindow::applyEntry(const NavEntry &entry) {
3048 if (entry.fileId == FileManager::InvalidFileID) {
3049 return;
3050 }
3051
3052 std::string_view path = fileManager_.getFilePath(entry.fileId);
3053 if (path.empty()) {
3054 logStatus(LogLevel::Warning, tr("History target file is unavailable"));
3055 return;
3056 }
3057
3058 suppressHistory_ = true;
3059 QString qPath = QString::fromStdString(std::string(path));
3060
3061 if (sourceView_->currentFileId() != entry.fileId) {
3062 if (!sourceView_->loadFile(qPath)) {
3063 logStatus(LogLevel::Error, QString("Failed to load %1").arg(qPath));
3064 suppressHistory_ = false;
3065 return;
3066 }
3067 sourceView_->setCurrentFileId(entry.fileId);
3068 updateSourceSubtitle(qPath);
3069 }
3070
3071 highlightTuFile(entry.fileId);
3072
3073 SourceLocation loc(entry.fileId, entry.line, entry.column);
3074 SourceRange range(loc, loc);
3075 sourceView_->highlightRange(range);
3076
3077 if (entry.astVersion == astVersion_ && entry.node) {
3078 QModelIndex modelIndex = astModel_->selectNode(entry.node);
3079 if (modelIndex.isValid()) {
3080 astView_->selectionModel()->setCurrentIndex(
3081 modelIndex, QItemSelectionModel::ClearAndSelect);
3082 astView_->scrollTo(modelIndex);
3083 }
3084 }
3085
3086 suppressHistory_ = false;
3087}
3088
3089void MainWindow::updateNavActions() {
3090 bool hasHistory = !history_.empty();
3091 if (navBackAction_) {
3092 navBackAction_->setEnabled(hasHistory && historyCursor_ > 0);
3093 if (hasHistory && historyCursor_ > 0) {
3094 const NavEntry &target = history_[historyCursor_ - 1];
3095 std::string_view path = fileManager_.getFilePath(target.fileId);
3096 navBackAction_->setToolTip(
3097 QString("Back to %1:%2")
3098 .arg(QString::fromStdString(std::string(path)))
3099 .arg(target.line));
3100 } else {
3101 navBackAction_->setToolTip(tr("Back"));
3102 }
3103 }
3104 if (navForwardAction_) {
3105 navForwardAction_->setEnabled(hasHistory &&
3106 (historyCursor_ + 1 < history_.size()));
3107 if (hasHistory && historyCursor_ + 1 < history_.size()) {
3108 const NavEntry &target = history_[historyCursor_ + 1];
3109 std::string_view path = fileManager_.getFilePath(target.fileId);
3110 navForwardAction_->setToolTip(
3111 QString("Forward to %1:%2")
3112 .arg(QString::fromStdString(std::string(path)))
3113 .arg(target.line));
3114 } else {
3115 navForwardAction_->setToolTip(tr("Forward"));
3116 }
3117 }
3118}
3119
3120void MainWindow::onTimingMessage(const QString &message) {
3121 QString trimmed = message.trimmed();
3122 if (trimmed.isEmpty()) {
3123 return;
3124 }
3125 // Skip legacy timing lines
3126 if (trimmed.startsWith(QStringLiteral("Timing "))) {
3127 return;
3128 }
3129 logStatus(LogLevel::Debug, trimmed, QStringLiteral("acav-timing"));
3130}
3131
3132// Navigation implementation
3133
3134void MainWindow::onAstNodeSelected(const QModelIndex &index) {
3135 if (!index.isValid() || !astContext_) {
3136 return;
3137 }
3138
3139 AstViewNode *node = static_cast<AstViewNode *>(index.internalPointer());
3140 if (!node) {
3141 return;
3142 }
3143
3144 if (suppressSourceHighlight_) {
3145 suppressSourceHighlight_ = false;
3146 astModel_->updateSelectionFromIndex(index);
3147 return;
3148 }
3149
3150 // Update model's selected node to keep selection state in sync
3151 astModel_->updateSelectionFromIndex(index);
3152
3153 const SourceRange &range = node->getSourceRange();
3154 if (goToMacroDefinitionAction_) {
3155 SourceRange macroRange(SourceLocation(FileManager::InvalidFileID, 0, 0),
3156 SourceLocation(FileManager::InvalidFileID, 0, 0));
3157 bool hasMacroRange = getMacroSpellingRange(node, &macroRange);
3158 goToMacroDefinitionAction_->setEnabled(hasMacroRange);
3159 }
3160 bool skipCursorMove = suppressSourceCursorMove_;
3161 suppressSourceCursorMove_ = false;
3162 navigateToRange(range, node, skipCursorMove);
3163}
3164
3165bool MainWindow::getMacroSpellingRange(const AstViewNode *node,
3166 SourceRange *outRange) const {
3167 if (!node || !outRange) {
3168 return false;
3169 }
3170 const auto &properties = node->getProperties();
3171 auto it = properties.find("macroSpellingRange");
3172 if (it == properties.end()) {
3173 return false;
3174 }
3175 return parseSourceRangeJson(*it, outRange);
3176}
3177
3178bool MainWindow::navigateToRange(const SourceRange &range, AstViewNode *node,
3179 bool skipCursorMove) {
3180 FileID rangeFileId = range.begin().fileID();
3181
3182 // Check for invalid location
3183 if (rangeFileId == FileManager::InvalidFileID) {
3184 // Node has no source location (e.g., TranslationUnitDecl)
3185 // Still highlight the main source file in file explorer for consistency
3186 if (!currentSourceFilePath_.isEmpty()) {
3187 if (auto fileId =
3188 fileManager_.tryGetFileId(currentSourceFilePath_.toStdString())) {
3189 highlightTuFile(*fileId);
3190 }
3191 }
3192 // Clear source highlight since there's no specific range
3193 sourceView_->clearHighlight();
3194 return false;
3195 }
3196
3197 // Check if range is in a different file
3198 if (rangeFileId != sourceView_->currentFileId()) {
3199 // Get file path from FileManager
3200 std::string_view filePath = fileManager_.getFilePath(rangeFileId);
3201 if (filePath.empty()) {
3202 logStatus(LogLevel::Warning, tr("Cannot find file path for AST node"));
3203 return false;
3204 }
3205
3206 // Load the file
3207 QString qFilePath = QString::fromStdString(std::string(filePath));
3208 if (!sourceView_->loadFile(qFilePath)) {
3209 logStatus(LogLevel::Error,
3210 QString("Failed to load file: %1").arg(qFilePath));
3211 return false;
3212 }
3213
3214 // Set the new file ID
3215 sourceView_->setCurrentFileId(rangeFileId);
3216 updateSourceSubtitle(qFilePath);
3217 }
3218
3219 // Highlight range in source view
3220 sourceView_->highlightRange(range, !skipCursorMove);
3221 highlightTuFile(rangeFileId);
3222
3223 if (node) {
3224 recordHistory(rangeFileId, range.begin().line(), range.begin().column(),
3225 node);
3226 }
3227
3228 return true;
3229}
3230
3231void MainWindow::onGoToMacroDefinition() {
3232 if (!astContext_ || !astView_) {
3233 return;
3234 }
3235 QModelIndex index = astView_->currentIndex();
3236 if (!index.isValid()) {
3237 return;
3238 }
3239 AstViewNode *node = static_cast<AstViewNode *>(index.internalPointer());
3240 if (!node) {
3241 return;
3242 }
3243 SourceRange macroRange(SourceLocation(FileManager::InvalidFileID, 0, 0),
3244 SourceLocation(FileManager::InvalidFileID, 0, 0));
3245 if (!getMacroSpellingRange(node, &macroRange)) {
3246 logStatus(LogLevel::Info, tr("No macro definition for this AST node"));
3247 return;
3248 }
3249 navigateToRange(macroRange, node, false);
3250}
3251
3252void MainWindow::onSourcePositionClicked(FileID fileId, unsigned line,
3253 unsigned column) {
3254 if (!astContext_ || fileId == FileManager::InvalidFileID) {
3255 return;
3256 }
3257 if (!validateSourceLookup(fileId)) {
3258 return;
3259 }
3260
3261 const auto &index = astContext_->getLocationIndex();
3262 auto matches = index.getNodesAt(fileId, line, column);
3263
3264 if (matches.empty()) {
3265 logNoNodeFound(fileId, tr("No AST node at this position"));
3266 return;
3267 }
3268
3269 // Filter to keep only the most specific nodes (smallest source ranges)
3270 // Step 1: Find the minimum range size among all matches
3271 struct RangeSize {
3272 unsigned lines;
3273 unsigned columns;
3274 };
3275
3276 auto getRangeSize = [](const SourceRange &range) -> RangeSize {
3277 unsigned lines = range.end().line() - range.begin().line();
3278 unsigned columns = 0;
3279 if (lines == 0) {
3280 // Same line - just column difference
3281 columns = range.end().column() - range.begin().column();
3282 } else {
3283 // Multi-line - use line count as primary measure
3284 columns = 0;
3285 }
3286 return {lines, columns};
3287 };
3288
3289 auto compareRangeSize = [](const RangeSize &a, const RangeSize &b) -> int {
3290 if (a.lines != b.lines)
3291 return a.lines < b.lines ? -1 : 1;
3292 if (a.columns != b.columns)
3293 return a.columns < b.columns ? -1 : 1;
3294 return 0; // Equal
3295 };
3296
3297 // Find the minimum range size
3298 RangeSize minSize = getRangeSize(matches[0]->getSourceRange());
3299 for (auto *node : matches) {
3300 RangeSize size = getRangeSize(node->getSourceRange());
3301 if (compareRangeSize(size, minSize) < 0) {
3302 minSize = size;
3303 }
3304 }
3305
3306 // Step 2: Keep only nodes with the minimum range size
3307 std::vector<AstViewNode *> mostSpecific;
3308 for (auto *node : matches) {
3309 RangeSize size = getRangeSize(node->getSourceRange());
3310 if (compareRangeSize(size, minSize) == 0) {
3311 mostSpecific.push_back(node);
3312 }
3313 }
3314
3315 // Pick the first (deepest) match from most-specific nodes
3316 AstViewNode *selected = mostSpecific.front();
3317
3318 QModelIndex modelIndex = astModel_->selectNode(selected);
3319 if (modelIndex.isValid()) {
3320 suppressSourceCursorMove_ = true;
3321 astView_->selectionModel()->setCurrentIndex(
3322 modelIndex, QItemSelectionModel::ClearAndSelect);
3323 astView_->scrollTo(modelIndex);
3324 suppressSourceCursorMove_ = false;
3325 const SourceRange &range = selected->getSourceRange();
3326 recordHistory(range.begin().fileID(), range.begin().line(),
3327 range.begin().column(), selected);
3328 }
3329}
3330
3331void MainWindow::onSourceRangeSelected(FileID fileId, unsigned startLine,
3332 unsigned startColumn, unsigned endLine,
3333 unsigned endColumn) {
3334 if (!astContext_ || fileId == FileManager::InvalidFileID) {
3335 return;
3336 }
3337 if (!validateSourceLookup(fileId)) {
3338 return;
3339 }
3340
3341 const auto &index = astContext_->getLocationIndex();
3342 AstViewNode *match = index.getFirstNodeContainedInRange(
3343 fileId, startLine, startColumn, endLine, endColumn);
3344
3345 if (!match) {
3346 logNoNodeFound(fileId, tr("No AST node within selection"));
3347 return;
3348 }
3349
3350 QModelIndex modelIndex = astModel_->selectNode(match);
3351 if (modelIndex.isValid()) {
3352 suppressSourceHighlight_ = true;
3353 astView_->selectionModel()->setCurrentIndex(
3354 modelIndex, QItemSelectionModel::ClearAndSelect);
3355 astView_->scrollTo(modelIndex);
3356 const SourceRange &range = match->getSourceRange();
3357 recordHistory(range.begin().fileID(), range.begin().line(),
3358 range.begin().column(), match);
3359 }
3360}
3361
3362void MainWindow::highlightTuFile(FileID fileId) {
3363 if (!tuModel_ || !tuView_) {
3364 return;
3365 }
3366
3367 QModelIndex tuIndex;
3368
3369 if (fileId != FileManager::InvalidFileID) {
3370 tuIndex = tuModel_->findIndexByFileId(fileId);
3371 }
3372
3373 if (!tuIndex.isValid() && fileId != FileManager::InvalidFileID) {
3374 if (!currentSourceFilePath_.isEmpty()) {
3375 std::string_view path = fileManager_.getFilePath(fileId);
3376 if (!path.empty()) {
3377 QString qPath = QString::fromStdString(std::string(path));
3378 QModelIndex sourceRoot =
3379 tuModel_->findIndexByFilePath(currentSourceFilePath_);
3380 if (sourceRoot.isValid()) {
3381 if (tuModel_->canFetchMore(sourceRoot)) {
3382 tuModel_->fetchMore(sourceRoot);
3383 }
3384 tuIndex = tuModel_->findIndexByAnyFilePathUnder(qPath, sourceRoot);
3385 }
3386 }
3387 }
3388 }
3389
3390 if (!tuIndex.isValid()) {
3391 return;
3392 }
3393
3394 // Ensure the path is visible by expanding ancestors
3395 QModelIndex parent = tuIndex.parent();
3396 while (parent.isValid()) {
3397 tuView_->expand(parent);
3398 parent = parent.parent();
3399 }
3400
3401 // Use selectionModel to set both current and selected state
3402 tuView_->selectionModel()->setCurrentIndex(
3403 tuIndex, QItemSelectionModel::ClearAndSelect);
3404 tuView_->scrollTo(tuIndex, QAbstractItemView::PositionAtCenter);
3405
3406 QRect rect = tuView_->visualRect(tuIndex);
3407 if (rect.isValid()) {
3408 int depth = 0;
3409 QModelIndex p = tuIndex.parent();
3410 while (p.isValid()) {
3411 ++depth;
3412 p = p.parent();
3413 }
3414
3415 QString text = tuIndex.data(Qt::DisplayRole).toString();
3416 QFontMetrics fm(tuView_->font());
3417 int textWidth = fm.horizontalAdvance(text);
3418
3419 int iconWidth = 0;
3420 QVariant iconVar = tuIndex.data(Qt::DecorationRole);
3421 if (iconVar.canConvert<QIcon>()) {
3422 QSize iconSize = tuView_->iconSize();
3423 if (iconSize.isValid()) {
3424 iconWidth = iconSize.width() + 4;
3425 }
3426 }
3427
3428 int indent = tuView_->indentation() * depth;
3429 int padding = 8;
3430 int textLeft = rect.left() + indent + iconWidth + padding;
3431 int textRight = textLeft + textWidth;
3432
3433 QScrollBar *hBar = tuView_->horizontalScrollBar();
3434 int viewWidth = tuView_->viewport()->width();
3435 if (textLeft < 0) {
3436 hBar->setValue(hBar->value() + textLeft);
3437 } else if (textRight > viewWidth) {
3438 hBar->setValue(hBar->value() + (textRight - viewWidth));
3439 }
3440 }
3441}
3442
3443void MainWindow::onCycleNodeSelected(AstViewNode *node) {
3444 if (!node) {
3445 return;
3446 }
3447
3448 QModelIndex modelIndex = astModel_->selectNode(node);
3449 if (modelIndex.isValid()) {
3450 astView_->selectionModel()->setCurrentIndex(
3451 modelIndex, QItemSelectionModel::ClearAndSelect);
3452 astView_->scrollTo(modelIndex);
3453
3454 // Also highlight in source
3455 const SourceRange &range = node->getSourceRange();
3456 highlightTuFile(range.begin().fileID());
3457 if (range.begin().fileID() == sourceView_->currentFileId()) {
3458 sourceView_->highlightRange(range);
3459 }
3460 recordHistory(range.begin().fileID(), range.begin().line(),
3461 range.begin().column(), node);
3462 }
3463}
3464
3465void MainWindow::onCycleWidgetClosed() {
3466 // Widget is closed, no special action needed
3467}
3468
3469void MainWindow::onExpandAllAstChildren() { expandAllChildren(astView_); }
3470
3471void MainWindow::onCollapseAllAstChildren() { collapseAllChildren(astView_); }
3472
3473namespace {
3474
3475bool isTuSourceNode(const QModelIndex &index) {
3476 return index.data(Qt::UserRole + 3).toBool();
3477}
3478
3480bool isSourceFileOrDescendant(const QModelIndex &index) {
3481 QModelIndex current = index;
3482 while (current.isValid()) {
3483 if (isTuSourceNode(current)) {
3484 return true;
3485 }
3486 current = current.parent();
3487 }
3488 return false;
3489}
3490
3491} // namespace
3492
3493void MainWindow::onTuContextMenuRequested(const QPoint &pos) {
3494 QModelIndex index = tuView_->indexAt(pos);
3495 if (!index.isValid()) {
3496 return;
3497 }
3498
3499 // Select the right-clicked item so actions operate on it
3500 tuView_->setCurrentIndex(index);
3501
3502 ::QMenu menu(tuView_);
3503
3504 // Context-aware expand/collapse:
3505 // - Directory nodes: expand/collapse directories only (down to source files)
3506 // - Source file or descendants: expand/collapse all descendants (incl.
3507 // headers)
3508 QAction *expandAction = menu.addAction(tr("Expand All"));
3509 QAction *collapseAction = menu.addAction(tr("Collapse All"));
3510
3511 connect(expandAction, &QAction::triggered, this,
3512 &MainWindow::onExpandAllTuChildren);
3513 connect(collapseAction, &QAction::triggered, this,
3514 &MainWindow::onCollapseAllTuChildren);
3515
3516 menu.exec(tuView_->viewport()->mapToGlobal(pos));
3517}
3518
3519void MainWindow::onExpandAllTuChildren() {
3520 QModelIndex currentIndex = tuView_->currentIndex();
3521 if (!currentIndex.isValid()) {
3522 return;
3523 }
3524
3525 // Context-aware expand:
3526 // - Inside source file subtree: expand all descendants (including headers)
3527 // - Directory level: expand directories only (down to source files)
3528 if (isSourceFileOrDescendant(currentIndex)) {
3529 expandSubtree(tuView_);
3530 } else {
3531 expandAllChildren(tuView_);
3532 }
3533}
3534
3535void MainWindow::onCollapseAllTuChildren() {
3536 QModelIndex currentIndex = tuView_->currentIndex();
3537 if (!currentIndex.isValid()) {
3538 return;
3539 }
3540
3541 // Context-aware collapse:
3542 // - Inside source file subtree: collapse all descendants
3543 // - Directory level: collapse directories only (down to source files)
3544 if (isSourceFileOrDescendant(currentIndex)) {
3545 collapseAllChildren(tuView_);
3546 } else {
3547 collapseTuDirectories(tuView_);
3548 }
3549}
3550
3551void MainWindow::expandAllChildren(QTreeView *view) {
3552 QModelIndex currentIndex = view->currentIndex();
3553 if (!currentIndex.isValid()) {
3554 return;
3555 }
3556
3557 // Temporarily disable ResizeToContents to avoid O(n) width recalculation
3558 QHeaderView::ResizeMode prevResizeMode = view->header()->sectionResizeMode(0);
3559 view->header()->setSectionResizeMode(QHeaderView::Fixed);
3560 view->setUpdatesEnabled(false);
3561
3562 // Block selection signals during batch expand to prevent
3563 // onAstNodeSelected firing for every intermediate expand.
3564 if (view->selectionModel()) {
3565 view->selectionModel()->blockSignals(true);
3566 }
3567
3568 if (view == tuView_) {
3569 // TU view: expand directory structure down to source-file leaf nodes,
3570 // without expanding source nodes (which would populate header subtrees).
3571 std::vector<QModelIndex> stack;
3572 stack.reserve(256);
3573 stack.push_back(currentIndex);
3574
3575 QAbstractItemModel *model = view->model();
3576 while (!stack.empty()) {
3577 QModelIndex idx = stack.back();
3578 stack.pop_back();
3579 if (!idx.isValid()) {
3580 continue;
3581 }
3582
3583 if (isTuSourceNode(idx)) {
3584 continue;
3585 }
3586
3587 int childCount = model->rowCount(idx);
3588 if (childCount <= 0) {
3589 continue;
3590 }
3591
3592 if (!view->isExpanded(idx)) {
3593 view->expand(idx);
3594 }
3595
3596 for (int row = 0; row < childCount; ++row) {
3597 stack.push_back(model->index(row, 0, idx));
3598 }
3599 }
3600 } else {
3601 // Expand subtree only (not the entire view).
3602 view->expandRecursively(currentIndex);
3603 }
3604
3605 if (view->selectionModel()) {
3606 view->selectionModel()->blockSignals(false);
3607 }
3608 view->setUpdatesEnabled(true);
3609 view->header()->setSectionResizeMode(prevResizeMode);
3610}
3611
3612void MainWindow::expandSubtree(QTreeView *view) {
3613 if (!view) {
3614 return;
3615 }
3616
3617 QModelIndex currentIndex = view->currentIndex();
3618 if (!currentIndex.isValid()) {
3619 return;
3620 }
3621
3622 QHeaderView::ResizeMode prevResizeMode = view->header()->sectionResizeMode(0);
3623 view->header()->setSectionResizeMode(QHeaderView::Fixed);
3624 view->setUpdatesEnabled(false);
3625 if (view->selectionModel()) {
3626 view->selectionModel()->blockSignals(true);
3627 }
3628
3629 QAbstractItemModel *model = view->model();
3630 std::vector<QModelIndex> stack;
3631 stack.reserve(256);
3632 stack.push_back(currentIndex);
3633
3634 while (!stack.empty()) {
3635 QModelIndex idx = stack.back();
3636 stack.pop_back();
3637 if (!idx.isValid()) {
3638 continue;
3639 }
3640
3641 if (model->canFetchMore(idx)) {
3642 model->fetchMore(idx);
3643 }
3644
3645 int childCount = model->rowCount(idx);
3646 if (childCount > 0 && !view->isExpanded(idx)) {
3647 view->expand(idx);
3648 }
3649
3650 for (int row = 0; row < childCount; ++row) {
3651 stack.push_back(model->index(row, 0, idx));
3652 }
3653 }
3654
3655 if (view->selectionModel()) {
3656 view->selectionModel()->blockSignals(false);
3657 }
3658 view->setUpdatesEnabled(true);
3659 view->header()->setSectionResizeMode(prevResizeMode);
3660}
3661
3662void MainWindow::collapseAllChildren(QTreeView *view) {
3663 QModelIndex currentIndex = view->currentIndex();
3664 if (!currentIndex.isValid()) {
3665 return;
3666 }
3667
3668 QHeaderView::ResizeMode prevResizeMode = view->header()->sectionResizeMode(0);
3669 view->header()->setSectionResizeMode(QHeaderView::Fixed);
3670 view->setUpdatesEnabled(false);
3671
3672 // Block selection signals during batch collapse to prevent
3673 // onAstNodeSelected + navigateToRange firing for every intermediate collapse.
3674 if (view->selectionModel()) {
3675 view->selectionModel()->blockSignals(true);
3676 }
3677
3678 if (!currentIndex.parent().isValid()) {
3679 // Root node: use Qt's built-in collapseAll() which is O(1) internally,
3680 // avoiding the O(N) individual collapse calls that hang on large ASTs.
3681 view->collapseAll();
3682 } else {
3683 // Subtree: collapse recursively from the selected node.
3684 collapseRecursively(currentIndex, view);
3685 }
3686
3687 if (view->selectionModel()) {
3688 view->selectionModel()->blockSignals(false);
3689 }
3690 view->setUpdatesEnabled(true);
3691 view->header()->setSectionResizeMode(prevResizeMode);
3692}
3693
3694void MainWindow::collapseTuDirectories(QTreeView *view) {
3695 if (!view) {
3696 return;
3697 }
3698
3699 QModelIndex currentIndex = view->currentIndex();
3700 if (!currentIndex.isValid()) {
3701 return;
3702 }
3703
3704 QHeaderView::ResizeMode prevResizeMode = view->header()->sectionResizeMode(0);
3705 view->header()->setSectionResizeMode(QHeaderView::Fixed);
3706 view->setUpdatesEnabled(false);
3707 if (view->selectionModel()) {
3708 view->selectionModel()->blockSignals(true);
3709 }
3710
3711 QAbstractItemModel *model = view->model();
3712 std::vector<QModelIndex> stack;
3713 std::vector<QModelIndex> postOrder;
3714 stack.reserve(256);
3715 postOrder.reserve(256);
3716 stack.push_back(currentIndex);
3717
3718 while (!stack.empty()) {
3719 QModelIndex idx = stack.back();
3720 stack.pop_back();
3721 if (!idx.isValid()) {
3722 continue;
3723 }
3724
3725 postOrder.push_back(idx);
3726
3727 int rowCount = model->rowCount(idx);
3728 for (int row = 0; row < rowCount; ++row) {
3729 QModelIndex child = model->index(row, 0, idx);
3730 // Traverse ALL expanded children including source files.
3731 // This ensures headers inside source files get collapsed too.
3732 if (view->isExpanded(child)) {
3733 stack.push_back(child);
3734 }
3735 }
3736 }
3737
3738 for (std::size_t i = postOrder.size(); i-- > 0;) {
3739 view->collapse(postOrder[i]);
3740 }
3741
3742 if (view->selectionModel()) {
3743 view->selectionModel()->blockSignals(false);
3744 }
3745
3746 view->setUpdatesEnabled(true);
3747 view->header()->setSectionResizeMode(prevResizeMode);
3748}
3749
3750void MainWindow::collapseRecursively(const QModelIndex &index,
3751 QTreeView *view) {
3752 if (!index.isValid() || !view) {
3753 return;
3754 }
3755
3756 QAbstractItemModel *model = view->model();
3757 std::vector<QModelIndex> stack;
3758 std::vector<QModelIndex> postOrder;
3759 stack.reserve(256);
3760 postOrder.reserve(256);
3761 stack.push_back(index);
3762
3763 while (!stack.empty()) {
3764 QModelIndex idx = stack.back();
3765 stack.pop_back();
3766 if (!idx.isValid()) {
3767 continue;
3768 }
3769
3770 postOrder.push_back(idx);
3771
3772 int rowCount = model->rowCount(idx);
3773 for (int row = 0; row < rowCount; ++row) {
3774 QModelIndex child = model->index(row, 0, idx);
3775 if (view->isExpanded(child)) {
3776 stack.push_back(child);
3777 }
3778 }
3779 }
3780
3781 for (std::size_t i = postOrder.size(); i-- > 0;) {
3782 view->collapse(postOrder[i]);
3783 }
3784}
3785
3786void MainWindow::updateSourceSubtitle(const QString &filePath) {
3787 if (!sourceTitleBar_ || filePath.isEmpty()) {
3788 if (sourceTitleBar_) {
3789 sourceTitleBar_->setSubtitle(QString());
3790 }
3791 return;
3792 }
3793
3794 QString projectRoot = tuModel_ ? tuModel_->projectRoot() : QString();
3795 QString subtitle;
3796
3797 if (!projectRoot.isEmpty() && filePath.startsWith(projectRoot + "/")) {
3798 // File is inside project - show relative path with [project] prefix
3799 QString relativePath = filePath.mid(projectRoot.length() + 1);
3800 subtitle = QStringLiteral("[project] %1").arg(relativePath);
3801 } else {
3802 // File is external - show absolute path with [external] prefix
3803 subtitle = QStringLiteral("[external] %1").arg(filePath);
3804 }
3805
3806 sourceTitleBar_->setSubtitle(subtitle);
3807}
3808
3809void MainWindow::updateAstSubtitle(const QString &mainSourcePath) {
3810 if (!astTitleBar_ || mainSourcePath.isEmpty()) {
3811 if (astTitleBar_) {
3812 astTitleBar_->setSubtitle(QString());
3813 }
3814 return;
3815 }
3816
3817 QString projectRoot = tuModel_ ? tuModel_->projectRoot() : QString();
3818 QString subtitle;
3819
3820 if (!projectRoot.isEmpty() && mainSourcePath.startsWith(projectRoot + "/")) {
3821 // File is inside project - show relative path with [project] prefix
3822 QString relativePath = mainSourcePath.mid(projectRoot.length() + 1);
3823 subtitle = QStringLiteral("[project] %1").arg(relativePath);
3824 } else {
3825 // File is external - show absolute path with [external] prefix
3826 subtitle = QStringLiteral("[external] %1").arg(mainSourcePath);
3827 }
3828
3829 astTitleBar_->setSubtitle(subtitle);
3830}
3831
3832} // namespace acav
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.
Definition AstNode.h:51
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.
Definition FileManager.h:38
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.
Definition AppConfig.h:42
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.