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