32#include <QAbstractItemView>
34#include <QApplication>
35#include <QButtonGroup>
40#include <QDesktopServices>
41#include <QDialogButtonBox>
48#include <QFontMetrics>
54#include <QKeySequence>
64#include <QPlainTextEdit>
66#include <QProgressDialog>
68#include <QRadioButton>
69#include <QRegularExpression>
72#include <QStringListModel>
76#include <QTableWidget>
79#include <QResizeEvent>
86#include <unordered_set>
91MainWindow::MainWindow(QWidget *parent)
92 : QMainWindow(parent), fileMenu_(nullptr), viewMenu_(nullptr),
93 openAction_(nullptr), exitAction_(nullptr), settingsAction_(nullptr),
94 reloadConfigAction_(nullptr), navBackAction_(nullptr),
95 navForwardAction_(nullptr), goToMacroDefinitionAction_(nullptr),
96 zoomInAction_(nullptr), zoomOutAction_(nullptr),
97 zoomResetAction_(nullptr), navToolBar_(nullptr),
98 resetLayoutAction_(nullptr), tuDock_(nullptr), sourceDock_(nullptr),
99 astDock_(nullptr), declContextDock_(nullptr), logDock_(nullptr),
100 tuView_(nullptr), sourceView_(nullptr), astView_(nullptr),
101 declContextView_(nullptr), tuModel_(nullptr), astModel_(nullptr),
102 queryRunner_(nullptr), parallelQueryRunner_(nullptr),
103 makeAstRunner_(nullptr), astExtractorRunner_(nullptr),
104 astWorkerThread_(nullptr), sourceSearchInput_(nullptr),
105 sourceSearchPrevButton_(nullptr), sourceSearchNextButton_(nullptr),
106 sourceSearchStatus_(nullptr), sourceSearchDebounce_(nullptr),
107 nodeCycleWidget_(nullptr), isAstExtractionInProgress_(false) {
109 qRegisterMetaType<LogEntry>();
113 setWindowTitle(
"ACAV");
114 setWindowIcon(QIcon(
":/resources/aurora_icon.png"));
125 applyFontSize(AppConfig::instance().getFontSize());
127 logStatus(LogLevel::Info, tr(
"Ready"));
130MainWindow::~MainWindow() {
131 QObject::disconnect(qApp, &QApplication::focusChanged,
this,
132 &MainWindow::onFocusChanged);
133 if (makeAstRunner_) {
134 QObject::disconnect(makeAstRunner_, &MakeAstRunner::logMessage,
this,
135 &MainWindow::onMakeAstLogMessage);
137 MemoryProfiler::setLogCallback(
nullptr);
141 astView_->setModel(
nullptr);
148 if (astWorkerThread_) {
149 astWorkerThread_->quit();
150 astWorkerThread_->wait();
152 if (astExportThread_) {
153 astExportThread_->quit();
154 astExportThread_->wait();
159void MainWindow::applyUnifiedSelectionPalette() {
160 QPalette pal = QApplication::palette();
161 const QBrush activeHighlight =
162 pal.brush(QPalette::Active, QPalette::Highlight);
163 const QBrush activeText =
164 pal.brush(QPalette::Active, QPalette::HighlightedText);
165 pal.setBrush(QPalette::Inactive, QPalette::Highlight, activeHighlight);
166 pal.setBrush(QPalette::Inactive, QPalette::HighlightedText, activeText);
167 QApplication::setPalette(pal);
170void MainWindow::setupViewMenuDockActions() {
175 viewMenu_->addSeparator();
177 const std::initializer_list<std::pair<QDockWidget *, QString>> docks = {
178 {tuDock_, tr(
"File Explorer")},
179 {sourceDock_, tr(
"Source Code")},
180 {astDock_, tr(
"AST")},
181 {declContextDock_, tr(
"Declaration Context")},
182 {logDock_, tr(
"Logs")}};
183 for (
const auto &[dock, text] : docks) {
185 QAction *action = dock->toggleViewAction();
186 action->setText(text);
187 viewMenu_->addAction(action);
191 viewMenu_->addSeparator();
193 resetLayoutAction_ =
new QAction(tr(
"Reset Layout"),
this);
194 resetLayoutAction_->setStatusTip(tr(
"Restore the default dock layout"));
195 connect(resetLayoutAction_, &QAction::triggered,
this,
196 &MainWindow::onResetLayout);
197 viewMenu_->addAction(resetLayoutAction_);
200void MainWindow::setupUI() {
201 applyUnifiedSelectionPalette();
203 setDockNestingEnabled(
true);
204 setDockOptions(QMainWindow::AnimatedDocks | QMainWindow::AllowNestedDocks |
205 QMainWindow::AllowTabbedDocks);
208 setCentralWidget(
nullptr);
211void MainWindow::setupMenuBar() {
213 fileMenu_ = menuBar()->addMenu(tr(
"&File"));
216 openAction_ =
new QAction(tr(
"Open &Project..."),
this);
217 openAction_->setShortcut(QKeySequence::Open);
218 openAction_->setStatusTip(tr(
"Open a compilation database file"));
219 fileMenu_->addAction(openAction_);
220 settingsAction_ =
new QAction(tr(
"Open Config File..."),
this);
221 settingsAction_->setShortcut(QKeySequence::Preferences);
222 settingsAction_->setStatusTip(
223 tr(
"Open configuration file in external editor"));
224 fileMenu_->addAction(settingsAction_);
225 connect(settingsAction_, &QAction::triggered,
this,
226 &MainWindow::onOpenConfigFile);
227 reloadConfigAction_ =
new QAction(tr(
"Reload Configuration"),
this);
228 reloadConfigAction_->setShortcut(
229 QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_R));
230 reloadConfigAction_->setStatusTip(
231 tr(
"Reload configuration file and reapply settings"));
232 fileMenu_->addAction(reloadConfigAction_);
233 connect(reloadConfigAction_, &QAction::triggered,
this,
234 &MainWindow::onReloadConfig);
236 fileMenu_->addSeparator();
239 exitAction_ =
new QAction(tr(
"E&xit"),
this);
240 exitAction_->setShortcut(QKeySequence::Quit);
241 exitAction_->setStatusTip(tr(
"Exit the application"));
242 fileMenu_->addAction(exitAction_);
245 viewMenu_ = menuBar()->addMenu(tr(
"&View"));
248 navBackAction_ =
new QAction(tr(
"Back"),
this);
249 navBackAction_->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_BracketLeft));
250 navBackAction_->setShortcutContext(Qt::ApplicationShortcut);
251 navBackAction_->setEnabled(
false);
252 navForwardAction_ =
new QAction(tr(
"Forward"),
this);
253 navForwardAction_->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_BracketRight));
254 navForwardAction_->setShortcutContext(Qt::ApplicationShortcut);
255 navForwardAction_->setEnabled(
false);
256 addAction(navBackAction_);
257 addAction(navForwardAction_);
258 connect(navBackAction_, &QAction::triggered,
this,
259 [
this]() { navigateHistory(-1); });
260 connect(navForwardAction_, &QAction::triggered,
this,
261 [
this]() { navigateHistory(1); });
263 goToMacroDefinitionAction_ =
new QAction(tr(
"Go to Macro Definition"),
this);
264 goToMacroDefinitionAction_->setShortcut(
265 QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_M));
266 goToMacroDefinitionAction_->setShortcutContext(Qt::ApplicationShortcut);
267 goToMacroDefinitionAction_->setEnabled(
false);
268 addAction(goToMacroDefinitionAction_);
269 viewMenu_->addAction(goToMacroDefinitionAction_);
270 connect(goToMacroDefinitionAction_, &QAction::triggered,
this,
271 [
this]() { onGoToMacroDefinition(); });
273 zoomInAction_ =
new QAction(tr(
"Zoom In"),
this);
274 zoomInAction_->setShortcuts({QKeySequence(Qt::CTRL | Qt::Key_Plus),
275 QKeySequence(Qt::CTRL | Qt::Key_Equal)});
276 zoomInAction_->setShortcutContext(Qt::ApplicationShortcut);
277 addAction(zoomInAction_);
278 viewMenu_->addAction(zoomInAction_);
279 connect(zoomInAction_, &QAction::triggered,
this,
280 [
this]() { adjustFontSize(1); });
282 zoomOutAction_ =
new QAction(tr(
"Zoom Out"),
this);
283 zoomOutAction_->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Minus));
284 zoomOutAction_->setShortcutContext(Qt::ApplicationShortcut);
285 addAction(zoomOutAction_);
286 viewMenu_->addAction(zoomOutAction_);
287 connect(zoomOutAction_, &QAction::triggered,
this,
288 [
this]() { adjustFontSize(-1); });
290 zoomResetAction_ =
new QAction(tr(
"Reset Zoom"),
this);
291 zoomResetAction_->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_0));
292 zoomResetAction_->setShortcutContext(Qt::ApplicationShortcut);
293 addAction(zoomResetAction_);
294 viewMenu_->addAction(zoomResetAction_);
295 connect(zoomResetAction_, &QAction::triggered,
this, [
this]() {
296 int defaultSize = AppConfig::instance().getFontSize();
297 currentFontFamily_ = AppConfig::instance().getFontFamily();
298 QFont baseFont = QApplication::font();
299 if (!currentFontFamily_.isEmpty()) {
300 baseFont.setFamily(currentFontFamily_);
302 baseFont.setPointSize(defaultSize);
305 if (focusedDock_ == tuDock_ && tuView_) {
306 tuFontSize_ = defaultSize;
307 tuView_->setFont(baseFont);
308 }
else if (focusedDock_ == sourceDock_ && sourceView_) {
309 sourceFontSize_ = defaultSize;
310 sourceView_->setFont(baseFont);
311 sourceView_->applyFontSize(defaultSize);
312 }
else if (focusedDock_ == astDock_ && astView_) {
313 astFontSize_ = defaultSize;
314 astView_->setFont(baseFont);
315 if (astSearchQuickInput_) {
316 astSearchQuickInput_->setFont(baseFont);
318 if (astSearchPopup_) {
319 astSearchPopup_->setFont(baseFont);
321 if (astSearchCompleter_ && astSearchCompleter_->popup()) {
322 astSearchCompleter_->popup()->setFont(baseFont);
324 }
else if (focusedDock_ == declContextDock_ && declContextView_) {
325 declContextFontSize_ = defaultSize;
326 declContextView_->applyFont(baseFont);
327 }
else if (focusedDock_ == logDock_) {
328 logFontSize_ = defaultSize;
329 logDock_->setFont(baseFont);
332 tuFontSize_ = defaultSize;
333 sourceFontSize_ = defaultSize;
334 astFontSize_ = defaultSize;
335 declContextFontSize_ = defaultSize;
336 logFontSize_ = defaultSize;
337 applyFontSize(defaultSize);
342 navToolBar_ = addToolBar(tr(
"Navigation"));
343 navToolBar_->addAction(navBackAction_);
344 navToolBar_->addAction(navForwardAction_);
345 navToolBar_->setMovable(
false);
348 ::QMenu *helpMenu = menuBar()->addMenu(tr(
"&Help"));
350 QAction *shortcutsAction =
new QAction(tr(
"&Keyboard Shortcuts"),
this);
351 shortcutsAction->setStatusTip(tr(
"Show keyboard shortcuts"));
352 connect(shortcutsAction, &QAction::triggered,
this, [
this]() {
353 auto *dialog =
new QDialog(
this);
354 dialog->setWindowTitle(tr(
"Keyboard Shortcuts"));
355 dialog->setAttribute(Qt::WA_DeleteOnClose);
357 QColor headerBg = palette().color(QPalette::Highlight);
358 QColor headerFg = palette().color(QPalette::HighlightedText);
360 auto makeTable = [&](
const QString &title) {
361 auto *group =
new QGroupBox(title, dialog);
362 auto *table =
new QTableWidget(group);
363 table->setColumnCount(2);
364 table->setHorizontalHeaderLabels({tr(
"Shortcut"), tr(
"Action")});
365 table->horizontalHeader()->setStretchLastSection(
true);
366 table->verticalHeader()->setVisible(
false);
367 table->setEditTriggers(QAbstractItemView::NoEditTriggers);
368 table->setSelectionMode(QAbstractItemView::NoSelection);
369 table->setFocusPolicy(Qt::NoFocus);
370 table->setShowGrid(
false);
371 table->setAlternatingRowColors(
true);
372 auto *layout =
new QVBoxLayout(group);
373 layout->setContentsMargins(0, 0, 0, 0);
374 layout->addWidget(table);
375 return std::make_pair(group, table);
378 auto addCategory = [&](QTableWidget *table,
const QString &name) {
379 int row = table->rowCount();
380 table->insertRow(row);
381 auto *item =
new QTableWidgetItem(name);
382 QFont f = item->font();
385 item->setBackground(headerBg);
386 item->setForeground(headerFg);
387 table->setItem(row, 0, item);
388 auto *spacer =
new QTableWidgetItem();
389 spacer->setBackground(headerBg);
390 table->setItem(row, 1, spacer);
391 table->setSpan(row, 0, 1, 2);
394 auto addShortcut = [](QTableWidget *table,
const QString &key,
395 const QString &action) {
396 int row = table->rowCount();
397 table->insertRow(row);
398 auto *keyItem =
new QTableWidgetItem(key);
399 QFont f = keyItem->font();
402 table->setItem(row, 0, keyItem);
403 table->setItem(row, 1,
new QTableWidgetItem(action));
406 auto finishTable = [](QTableWidget *table) {
407 table->resizeColumnsToContents();
408 table->resizeRowsToContents();
409 table->setMinimumWidth(table->columnWidth(0) + table->columnWidth(1) +
414 auto [leftGroup, leftTable] = makeTable(tr(
""));
415 addCategory(leftTable, tr(
"Focus Switching"));
416 addShortcut(leftTable,
"Tab", tr(
"Cycle focus between panes"));
417 addShortcut(leftTable,
"Ctrl+1", tr(
"File Explorer"));
418 addShortcut(leftTable,
"Ctrl+2", tr(
"Source Code"));
419 addShortcut(leftTable,
"Ctrl+3", tr(
"AST"));
420 addShortcut(leftTable,
"Ctrl+4", tr(
"Decl Context (Semantic)"));
421 addShortcut(leftTable,
"Ctrl+5", tr(
"Decl Context (Lexical)"));
422 addShortcut(leftTable,
"Ctrl+6", tr(
"Logs"));
423 addCategory(leftTable, tr(
"Tree Views"));
424 addShortcut(leftTable,
"Ctrl+Shift+E", tr(
"Expand all children"));
425 addShortcut(leftTable,
"Ctrl+Shift+C", tr(
"Collapse all children"));
426 addShortcut(leftTable,
"F5", tr(
"Extract AST for selected file"));
427 addCategory(leftTable, tr(
"Search"));
428 addShortcut(leftTable,
"Ctrl+F", tr(
"Focus search in active panel"));
429 addCategory(leftTable, tr(
"Source Code"));
430 addShortcut(leftTable,
"Home", tr(
"Go to start of file"));
431 addShortcut(leftTable,
"End", tr(
"Go to end of file"));
432 finishTable(leftTable);
435 auto [rightGroup, rightTable] = makeTable(tr(
""));
436 addCategory(rightTable, tr(
"View"));
437 addShortcut(rightTable,
"Ctrl++", tr(
"Zoom in"));
438 addShortcut(rightTable,
"Ctrl+-", tr(
"Zoom out"));
439 addShortcut(rightTable,
"Ctrl+0", tr(
"Reset zoom"));
440 addCategory(rightTable, tr(
"File"));
441 addShortcut(rightTable,
"Ctrl+O", tr(
"Open project"));
442 addShortcut(rightTable,
"Ctrl+Shift+R", tr(
"Reload configuration"));
443 addShortcut(rightTable,
"Ctrl+Q", tr(
"Quit"));
444 addCategory(rightTable, tr(
"Navigation"));
445 addShortcut(rightTable,
"Ctrl+[", tr(
"Navigate back"));
446 addShortcut(rightTable,
"Ctrl+]", tr(
"Navigate forward"));
447 addCategory(rightTable, tr(
"AST"));
448 addShortcut(rightTable,
"Ctrl+Shift+M", tr(
"Go to macro definition"));
449 addShortcut(rightTable,
"Ctrl+I", tr(
"Inspect node details"));
450 finishTable(rightTable);
452 auto *columnsLayout =
new QHBoxLayout;
453 columnsLayout->addWidget(leftGroup);
454 columnsLayout->addWidget(rightGroup);
456 auto *closeButton =
new QPushButton(tr(
"Close"), dialog);
457 connect(closeButton, &QPushButton::clicked, dialog, &QDialog::close);
459 auto *buttonLayout =
new QHBoxLayout;
460 buttonLayout->addStretch();
461 buttonLayout->addWidget(closeButton);
463 auto *mainLayout =
new QVBoxLayout(dialog);
464 mainLayout->addLayout(columnsLayout);
465 mainLayout->addLayout(buttonLayout);
467 dialog->resize(680, 480);
470 helpMenu->addAction(shortcutsAction);
472 helpMenu->addSeparator();
474 QAction *aboutAction =
new QAction(tr(
"&About"),
this);
475 aboutAction->setStatusTip(tr(
"About this application"));
476 connect(aboutAction, &QAction::triggered,
this, &MainWindow::onShowAbout);
477 helpMenu->addAction(aboutAction);
480void MainWindow::onOpenConfigFile() {
481 QString configPath = AppConfig::instance().getConfigFilePath();
483 QFileInfo fileInfo(configPath);
484 if (!fileInfo.exists()) {
485 QMessageBox::warning(
this, tr(
"Config File Not Found"),
486 tr(
"Configuration file not found:\n%1\n\n"
487 "The file should have been created automatically. "
488 "Try restarting the application.")
494 QUrl fileUrl = QUrl::fromLocalFile(configPath);
495 if (!QDesktopServices::openUrl(fileUrl)) {
496 QMessageBox::information(
497 this, tr(
"Open Config File"),
498 tr(
"Could not open the configuration file automatically.\n\n"
499 "Please open it manually with a text editor:\n%1")
502 logStatus(LogLevel::Info,
503 tr(
"Opened config file: %1").arg(fileInfo.fileName()));
507void MainWindow::onReloadConfig() {
508 AppConfig &config = AppConfig::instance();
509 const bool configAvailable = config.reload();
511 applyFontSize(config.getFontSize());
513 if (parallelQueryRunner_) {
514 parallelQueryRunner_->setParallelCount(config.getParallelProcessorCount());
516 if (astExtractorRunner_) {
517 astExtractorRunner_->setCommentExtractionEnabled(
518 config.getCommentExtractionEnabled());
521 if (!configAvailable) {
522 logStatus(LogLevel::Warning,
523 tr(
"Config file missing; created defaults and reloaded."));
527 logStatus(LogLevel::Info,
528 tr(
"Reloaded configuration: %1").arg(config.getConfigFilePath()));
531void MainWindow::onResetLayout() {
532 if (defaultDockState_.isEmpty()) {
535 restoreState(defaultDockState_);
538void MainWindow::onFocusChanged(QWidget *old, QWidget *now) {
542 const std::initializer_list<std::pair<QDockWidget *, DockTitleBar *>>
543 dockTitleBars = {{tuDock_, tuTitleBar_},
544 {sourceDock_, sourceTitleBar_},
545 {astDock_, astTitleBar_},
546 {declContextDock_, declContextTitleBar_},
547 {logDock_, logTitleBar_}};
550 QDockWidget *activeDock =
nullptr;
551 if (now && astSearchPopup_ && astSearchPopup_->isAncestorOf(now)) {
552 activeDock = astDock_;
554 for (
const auto &[dock, titleBar] : dockTitleBars) {
558 if (now && dock && dock->isAncestorOf(now)) {
564 if (focusedDock_ != activeDock) {
566 for (
const auto &[dock, titleBar] : dockTitleBars) {
568 titleBar->setFocused(dock == activeDock);
571 focusedDock_ = activeDock;
575void MainWindow::resizeEvent(QResizeEvent *event) {
576 QMainWindow::resizeEvent(event);
577 if (astSearchPopup_ && astSearchPopup_->isVisible()) {
578 syncAstSearchPopupGeometry();
582void MainWindow::moveEvent(QMoveEvent *event) {
583 QMainWindow::moveEvent(event);
584 if (astSearchPopup_ && astSearchPopup_->isVisible()) {
585 syncAstSearchPopupGeometry();
589bool MainWindow::eventFilter(QObject *watched, QEvent *event) {
590 if (watched == astDock_ && astSearchPopup_ && astSearchPopup_->isVisible()) {
591 switch (event->type()) {
596 syncAstSearchPopupGeometry();
602 return QMainWindow::eventFilter(watched, event);
605void MainWindow::onShowAbout() {
607 tr(
"<h2>ACAV (Clang AST Viewer)</h2>"
608 "<p><b>Version:</b> %1</p>"
609 "<p><b>Organization:</b> University of Victoria</p>"
610 "<p><b>Supervisor:</b> Professor Michael Adams</p>"
611 "<p><b>Developer:</b> Min Liu</p>"
613 "<p>A tool for visualizing and exploring Clang Abstract Syntax Trees "
615 "with support for C++20 modules.</p>"
616 "<p><b>Features:</b></p>"
618 "<li>Load and display AST from compilation databases</li>"
619 "<li>Bidirectional navigation between source code and AST</li>"
620 "<li>Support for C++20 modules</li>"
622 "<p>Built with Qt and LLVM/Clang.</p>")
623 .arg(ACAV_VERSION_STRING);
625 QMessageBox::about(
this, tr(
"About ACAV"), aboutText);
630class HistoryLineEdit :
public QLineEdit {
632 explicit HistoryLineEdit(QWidget *parent =
nullptr) : QLineEdit(parent) {}
635 void focusInEvent(QFocusEvent *event)
override {
636 QLineEdit::focusInEvent(event);
640 void mousePressEvent(QMouseEvent *event)
override {
641 QLineEdit::mousePressEvent(event);
642 if (event->button() == Qt::LeftButton) {
648 void showHistoryPopup() {
649 QCompleter *historyCompleter = completer();
650 if (!historyCompleter) {
653 historyCompleter->setCompletionPrefix(text().trimmed());
654 historyCompleter->complete();
658AcavJson sourceLocationToJson(
const SourceLocation &loc,
659 const FileManager &fileManager) {
661 if (!loc.isValid()) {
662 obj[
"valid"] =
false;
667 obj[
"fileId"] = loc.fileID();
668 obj[
"line"] = loc.line();
669 obj[
"column"] = loc.column();
671 std::string_view filePath = fileManager.getFilePath(loc.fileID());
672 if (!filePath.empty()) {
673 obj[
"filePath"] = InternedString(std::string(filePath));
679AcavJson sourceRangeToJson(
const SourceRange &range,
680 const FileManager &fileManager) {
682 obj[
"begin"] = sourceLocationToJson(range.begin(), fileManager);
683 obj[
"end"] = sourceLocationToJson(range.end(), fileManager);
687bool parseSourceLocationJson(
const AcavJson &obj, SourceLocation *out) {
688 if (!out || !obj.is_object()) {
691 auto fileIdIt = obj.find(
"fileId");
692 auto lineIt = obj.find(
"line");
693 auto columnIt = obj.find(
"column");
694 if (fileIdIt == obj.end() || lineIt == obj.end() || columnIt == obj.end()) {
697 if (!fileIdIt->is_number_integer() || !lineIt->is_number_integer() ||
698 !columnIt->is_number_integer()) {
702 FileID fileId =
static_cast<FileID>(fileIdIt->get<uint64_t>());
703 unsigned line =
static_cast<unsigned>(lineIt->get<uint64_t>());
704 unsigned column =
static_cast<unsigned>(columnIt->get<uint64_t>());
705 if (fileId == FileManager::InvalidFileID || line == 0 || column == 0) {
709 *out = SourceLocation(fileId, line, column);
713bool parseSourceRangeJson(
const AcavJson &obj, SourceRange *out) {
714 if (!out || !obj.is_object()) {
717 auto beginIt = obj.find(
"begin");
718 auto endIt = obj.find(
"end");
719 if (beginIt == obj.end() || endIt == obj.end()) {
722 SourceLocation begin(FileManager::InvalidFileID, 0, 0);
723 SourceLocation end(FileManager::InvalidFileID, 0, 0);
724 if (!parseSourceLocationJson(*beginIt, &begin) ||
725 !parseSourceLocationJson(*endIt, &end)) {
728 *out = SourceRange(begin, end);
732AcavJson buildAstJsonTree(AstViewNode *root,
const FileManager &fileManager) {
737 auto makeNodeJson = [&fileManager](AstViewNode *node) {
739 obj[
"properties"] = node->getProperties();
740 obj[
"sourceRange"] = sourceRangeToJson(node->getSourceRange(), fileManager);
744 AcavJson rootJson = makeNodeJson(root);
745 std::vector<std::pair<AstViewNode *, AcavJson *>> stack;
747 stack.push_back({root, &rootJson});
749 while (!stack.empty()) {
750 auto [node, jsonPtr] = stack.back();
753 const auto &children = node->getChildren();
754 AcavJson childArray = AcavJson::array();
755 childArray.get_ref<AcavJson::array_t &>().reserve(children.size());
756 (*jsonPtr)[
"children"] = std::move(childArray);
757 auto &storedChildren = (*jsonPtr)[
"children"];
759 for (AstViewNode *child : children) {
763 AcavJson childJson = makeNodeJson(child);
764 storedChildren.push_back(childJson);
765 stack.push_back({child, &storedChildren.back()});
774void MainWindow::setupDockWidgets() {
777 tuView_ =
new QTreeView(
this);
778 sourceView_ =
new SourceCodeView(
this);
779 astView_ =
new QTreeView(
this);
782 tuDock_ =
new QDockWidget(
this);
783 tuDock_->setObjectName(
"tuDock");
784 tuDock_->setAllowedAreas(Qt::AllDockWidgetAreas);
785 tuTitleBar_ =
new DockTitleBar(tr(
"File Explorer"), tuDock_);
786 tuDock_->setTitleBarWidget(tuTitleBar_);
788 sourceDock_ =
new QDockWidget(
this);
789 sourceDock_->setObjectName(
"sourceDock");
790 sourceDock_->setAllowedAreas(Qt::AllDockWidgetAreas);
791 sourceTitleBar_ =
new DockTitleBar(tr(
"Source Code"), sourceDock_);
792 sourceDock_->setTitleBarWidget(sourceTitleBar_);
793 QWidget *sourceContainer =
new QWidget(sourceDock_);
794 setupSourceSearchPanel(sourceContainer);
795 sourceDock_->setWidget(sourceContainer);
797 astDock_ =
new QDockWidget(
this);
798 astDock_->setObjectName(
"astDock");
799 astDock_->setAllowedAreas(Qt::AllDockWidgetAreas);
800 astTitleBar_ =
new DockTitleBar(tr(
"AST"), astDock_);
801 astDock_->setTitleBarWidget(astTitleBar_);
802 QWidget *astContainer =
new QWidget(astDock_);
803 setupAstSearchPanel(astContainer);
804 astDock_->setWidget(astContainer);
809 declContextDock_ =
new QDockWidget(
this);
810 declContextDock_->setObjectName(
"declContextDock");
811 declContextDock_->setAllowedAreas(Qt::AllDockWidgetAreas);
812 declContextTitleBar_ =
813 new DockTitleBar(tr(
"Declaration Context"), declContextDock_);
814 declContextDock_->setTitleBarWidget(declContextTitleBar_);
815 declContextView_ =
new DeclContextView(
this);
816 declContextDock_->setWidget(declContextView_);
818 logDock_ =
new LogDock(
this);
819 logDock_->setAllowedAreas(Qt::AllDockWidgetAreas);
820 logTitleBar_ =
new DockTitleBar(tr(
"Logs"), logDock_);
821 logDock_->setTitleBarWidget(logTitleBar_);
822 logDock_->setFeatures(logDock_->features() | QDockWidget::DockWidgetMovable |
823 QDockWidget::DockWidgetFloatable);
824 QPointer<LogDock> logDockPtr = logDock_;
825 MemoryProfiler::setLogCallback([logDockPtr](
const QString &message) {
830 entry.level = LogLevel::Debug;
831 entry.source = QStringLiteral(
"acav-memory");
832 entry.message = message;
833 entry.timestamp = QDateTime::currentDateTime();
834 QMetaObject::invokeMethod(logDockPtr,
"enqueue", Qt::QueuedConnection,
835 Q_ARG(LogEntry, entry));
840 addDockWidget(Qt::TopDockWidgetArea, tuDock_);
841 addDockWidget(Qt::TopDockWidgetArea, sourceDock_);
842 addDockWidget(Qt::TopDockWidgetArea, astDock_);
843 addDockWidget(Qt::TopDockWidgetArea, declContextDock_);
844 addDockWidget(Qt::BottomDockWidgetArea, logDock_);
849 splitDockWidget(tuDock_, sourceDock_, Qt::Horizontal);
850 splitDockWidget(sourceDock_, astDock_, Qt::Horizontal);
851 splitDockWidget(astDock_, declContextDock_, Qt::Horizontal);
855 resizeDocks({tuDock_, sourceDock_, astDock_, declContextDock_},
856 {300, 450, 450, 200}, Qt::Horizontal);
859 const int lineHeight = QFontMetrics(logDock_->font()).lineSpacing();
860 const int logDockHeight =
862 const int topDockHeight =
863 900 - logDockHeight - 60;
864 resizeDocks({sourceDock_, logDock_}, {topDockHeight, logDockHeight},
867 setupViewMenuDockActions();
868 ::QTimer::singleShot(0,
this, [
this]() { defaultDockState_ = saveState(); });
871void MainWindow::setupSourceSearchPanel(QWidget *container) {
876 auto *outerLayout =
new QVBoxLayout(container);
877 outerLayout->setContentsMargins(4, 4, 4, 4);
878 outerLayout->setSpacing(4);
880 auto *controlsLayout =
new QHBoxLayout();
881 controlsLayout->setContentsMargins(0, 0, 0, 0);
882 controlsLayout->setSpacing(4);
884 sourceSearchInput_ =
new QLineEdit(container);
885 sourceSearchInput_->setPlaceholderText(tr(
"Search source code..."));
886 sourceSearchInput_->setClearButtonEnabled(
true);
887 sourceSearchInput_->setProperty(
"searchField",
true);
888 sourceSearchInput_->setMinimumHeight(28);
890 sourceSearchPrevButton_ =
new QToolButton(container);
891 sourceSearchPrevButton_->setText(tr(
"Prev"));
892 sourceSearchPrevButton_->setEnabled(
false);
893 sourceSearchPrevButton_->setProperty(
"searchButton",
true);
894 sourceSearchPrevButton_->setMinimumHeight(28);
896 sourceSearchNextButton_ =
new QToolButton(container);
897 sourceSearchNextButton_->setText(tr(
"Next"));
898 sourceSearchNextButton_->setEnabled(
false);
899 sourceSearchNextButton_->setProperty(
"searchButton",
true);
900 sourceSearchNextButton_->setMinimumHeight(28);
902 sourceSearchStatus_ =
new QLabel(container);
903 sourceSearchStatus_->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
904 sourceSearchStatus_->setMinimumWidth(80);
905 sourceSearchStatus_->setProperty(
"searchStatus",
true);
907 sourceSearchDebounce_ = new ::QTimer(container);
908 sourceSearchDebounce_->setSingleShot(
true);
909 sourceSearchDebounce_->setInterval(200);
911 controlsLayout->addWidget(sourceSearchInput_, 1);
912 controlsLayout->addWidget(sourceSearchPrevButton_);
913 controlsLayout->addWidget(sourceSearchNextButton_);
914 controlsLayout->addWidget(sourceSearchStatus_);
916 outerLayout->addLayout(controlsLayout);
917 outerLayout->addWidget(sourceView_, 1);
919 connect(sourceSearchInput_, &QLineEdit::returnPressed,
this,
920 &MainWindow::onSourceSearchFindNext);
921 connect(sourceSearchInput_, &QLineEdit::textChanged,
this,
922 &MainWindow::onSourceSearchTextChanged);
923 connect(sourceSearchDebounce_, &::QTimer::timeout,
this,
924 &MainWindow::onSourceSearchDebounced);
925 connect(sourceSearchPrevButton_, &QToolButton::clicked,
this,
926 &MainWindow::onSourceSearchFindPrevious);
927 connect(sourceSearchNextButton_, &QToolButton::clicked,
this,
928 &MainWindow::onSourceSearchFindNext);
931void MainWindow::setupAstSearchPanel(QWidget *container) {
936 auto *outerLayout =
new QVBoxLayout(container);
937 outerLayout->setContentsMargins(8, 8, 8, 8);
938 outerLayout->setSpacing(8);
941 auto *quickSearchFrame =
new QWidget(container);
942 quickSearchFrame->setObjectName(
"astQuickSearchFrame");
944 auto *quickLayout =
new QHBoxLayout(quickSearchFrame);
945 quickLayout->setContentsMargins(8, 6, 8, 6);
946 quickLayout->setSpacing(6);
948 astSearchQuickInput_ =
new QLineEdit(quickSearchFrame);
949 astSearchQuickInput_->setObjectName(
"astSearchQuickInput");
950 astSearchQuickInput_->setPlaceholderText(tr(
"Search AST nodes..."));
951 astSearchQuickInput_->setClearButtonEnabled(
true);
952 astSearchQuickInput_->setProperty(
"searchField",
true);
953 astSearchQuickInput_->setMinimumHeight(28);
955 auto *quickSearchButton =
new QToolButton(quickSearchFrame);
956 quickSearchButton->setObjectName(
"astSearchQuickButton");
957 quickSearchButton->setText(tr(
"Advanced"));
958 quickSearchButton->setToolTip(tr(
"Open advanced search options (Ctrl+F)"));
959 quickSearchButton->setProperty(
"searchButton",
true);
960 quickSearchButton->setMinimumHeight(28);
961 quickSearchButton->setMinimumWidth(90);
963 quickLayout->addWidget(astSearchQuickInput_, 1);
964 quickLayout->addWidget(quickSearchButton);
967 astSearchPopup_ =
new QDialog(
this);
968 astSearchPopup_->setObjectName(
"astSearchPopup");
969 astSearchPopup_->setWindowTitle(tr(
"AST Advanced Search"));
970 astSearchPopup_->setModal(
false);
971 astSearchPopup_->setWindowModality(Qt::NonModal);
972 astSearchPopup_->setMinimumSize(320, 180);
973 astSearchPopup_->resize(600, 220);
975 auto *popupLayout =
new QVBoxLayout(astSearchPopup_);
976 popupLayout->setContentsMargins(12, 12, 12, 12);
977 popupLayout->setSpacing(10);
980 auto *inputLabel =
new QLabel(tr(
"<b>Search Pattern:</b>"), astSearchPopup_);
981 popupLayout->addWidget(inputLabel);
983 astSearchInput_ =
new HistoryLineEdit(astSearchPopup_);
984 astSearchInput_->setObjectName(
"astSearchInput");
985 astSearchInput_->setPlaceholderText(
986 tr(
"e.g., kind:FunctionDecl name:main type:.*int.*"));
987 astSearchInput_->setClearButtonEnabled(
true);
988 astSearchInput_->setProperty(
"searchField",
true);
989 astSearchInput_->setMinimumHeight(32);
990 astSearchInput_->setToolTip(
991 tr(
"Use qualifiers like kind:, name:, type: for precise matching.\n"
992 "Supports regex patterns.\n"
993 "Examples: kind:FunctionDecl name:main type:.*int.*"));
995 astSearchHistoryModel_ =
new QStringListModel(astSearchPopup_);
996 astSearchCompleter_ =
new QCompleter(astSearchHistoryModel_, astSearchPopup_);
997 astSearchCompleter_->setCaseSensitivity(Qt::CaseInsensitive);
998 astSearchCompleter_->setCompletionMode(QCompleter::PopupCompletion);
999 astSearchCompleter_->setFilterMode(Qt::MatchContains);
1000 astSearchInput_->setCompleter(astSearchCompleter_);
1002 popupLayout->addWidget(astSearchInput_);
1005 auto *separator =
new QFrame(astSearchPopup_);
1006 separator->setFrameShape(QFrame::HLine);
1007 separator->setFrameShadow(QFrame::Sunken);
1008 popupLayout->addWidget(separator);
1011 auto *controlsFrame =
new QWidget(astSearchPopup_);
1012 auto *popupControlsLayout =
new QHBoxLayout(controlsFrame);
1013 popupControlsLayout->setContentsMargins(0, 0, 0, 0);
1014 popupControlsLayout->setSpacing(8);
1017 astSearchProjectFilter_ =
new QCheckBox(tr(
"Project Files Only"), astSearchPopup_);
1018 astSearchProjectFilter_->setChecked(
true);
1019 astSearchProjectFilter_->setToolTip(tr(
"Restrict search to files within the project root"));
1021 popupControlsLayout->addWidget(astSearchProjectFilter_);
1022 popupControlsLayout->addStretch(1);
1025 astSearchPrevButton_ =
new QToolButton(astSearchPopup_);
1026 astSearchPrevButton_->setText(tr(
"Previous"));
1027 astSearchPrevButton_->setToolTip(tr(
"Find previous match (Shift+Enter)"));
1028 astSearchPrevButton_->setEnabled(
false);
1029 astSearchPrevButton_->setProperty(
"searchButton",
true);
1030 astSearchPrevButton_->setMinimumWidth(90);
1031 astSearchPrevButton_->setMinimumHeight(28);
1033 astSearchNextButton_ =
new QToolButton(astSearchPopup_);
1034 astSearchNextButton_->setText(tr(
"Next"));
1035 astSearchNextButton_->setToolTip(tr(
"Find next match (Enter)"));
1036 astSearchNextButton_->setEnabled(
false);
1037 astSearchNextButton_->setProperty(
"searchButton",
true);
1038 astSearchNextButton_->setMinimumWidth(90);
1039 astSearchNextButton_->setMinimumHeight(28);
1041 auto *astSearchButton =
new QToolButton(astSearchPopup_);
1042 astSearchButton->setText(tr(
"Search"));
1043 astSearchButton->setToolTip(tr(
"Execute search"));
1044 astSearchButton->setProperty(
"searchButton",
true);
1045 astSearchButton->setMinimumWidth(90);
1046 astSearchButton->setMinimumHeight(28);
1048 popupControlsLayout->addWidget(astSearchPrevButton_);
1049 popupControlsLayout->addWidget(astSearchNextButton_);
1050 popupControlsLayout->addWidget(astSearchButton);
1052 popupLayout->addWidget(controlsFrame);
1055 astSearchStatus_ =
new QLabel(astSearchPopup_);
1056 astSearchStatus_->setObjectName(
"astSearchStatus");
1057 astSearchStatus_->setAlignment(Qt::AlignCenter);
1058 astSearchStatus_->setMinimumHeight(24);
1059 astSearchStatus_->setProperty(
"searchStatus",
true);
1060 popupLayout->addWidget(astSearchStatus_);
1063 astCompilationWarningLabel_ =
new QLabel(container);
1064 astCompilationWarningLabel_->setObjectName(
"astCompilationWarningLabel");
1065 astCompilationWarningLabel_->setText(
1066 tr(
"<b>Compilation Errors Detected:</b> AST may be incomplete. "
1067 "See log for details."));
1068 astCompilationWarningLabel_->setTextFormat(Qt::RichText);
1069 astCompilationWarningLabel_->setWordWrap(
true);
1071 astCompilationWarningLabel_->setVisible(
false);
1073 outerLayout->addWidget(quickSearchFrame);
1074 outerLayout->addWidget(astCompilationWarningLabel_);
1075 outerLayout->addWidget(astView_, 1);
1077 connect(astSearchQuickInput_, &QLineEdit::textChanged,
this,
1078 [
this](
const QString &text) {
1079 if (astSearchInput_ && astSearchInput_->text() != text) {
1080 astSearchInput_->setText(text);
1083 auto triggerQuickSearch = [
this]() {
1084 if (!astSearchInput_ || !astSearchQuickInput_) {
1087 const QString query = astSearchQuickInput_->text();
1088 if (astSearchInput_->text() != query) {
1089 astSearchInput_->setText(query);
1091 showAstSearchPopup(
false);
1092 onAstSearchFindNext();
1094 connect(astSearchQuickInput_, &QLineEdit::returnPressed,
this,
1095 triggerQuickSearch);
1096 connect(quickSearchButton, &QToolButton::clicked,
this, triggerQuickSearch);
1097 connect(astSearchInput_, &QLineEdit::returnPressed,
this,
1098 &MainWindow::onAstSearchFindNext);
1099 connect(astSearchInput_, &QLineEdit::textChanged,
this,
1100 &MainWindow::onAstSearchTextChanged);
1101 connect(astSearchInput_, &QLineEdit::textChanged,
this,
1102 [
this](
const QString &text) {
1103 if (astSearchQuickInput_ && astSearchQuickInput_->text() != text) {
1104 astSearchQuickInput_->setText(text);
1107 connect(astSearchButton, &QToolButton::clicked,
this,
1108 &MainWindow::onAstSearchFindNext);
1109 connect(astSearchPrevButton_, &QToolButton::clicked,
this,
1110 &MainWindow::onAstSearchFindPrevious);
1111 connect(astSearchNextButton_, &QToolButton::clicked,
this,
1112 &MainWindow::onAstSearchFindNext);
1113 connect(astSearchProjectFilter_, &QCheckBox::toggled,
this,
1114 &MainWindow::clearAstSearchState);
1116 auto *prevShortcut =
1117 new QShortcut(QKeySequence(Qt::SHIFT | Qt::Key_Return), astSearchPopup_);
1118 connect(prevShortcut, &QShortcut::activated,
this,
1119 &MainWindow::onAstSearchFindPrevious);
1120 auto *prevNumpadShortcut =
1121 new QShortcut(QKeySequence(Qt::SHIFT | Qt::Key_Enter), astSearchPopup_);
1122 connect(prevNumpadShortcut, &QShortcut::activated,
this,
1123 &MainWindow::onAstSearchFindPrevious);
1124 auto *closeShortcut =
new QShortcut(QKeySequence(Qt::Key_Escape),
1126 connect(closeShortcut, &QShortcut::activated, astSearchPopup_,
1130 astDock_->installEventFilter(
this);
1134 QFont popupFont = QApplication::font();
1135 popupFont.setPointSize(AppConfig::instance().getFontSize());
1136 QString configuredFamily = AppConfig::instance().getFontFamily();
1137 if (!configuredFamily.isEmpty()) {
1138 popupFont.setFamily(configuredFamily);
1140 astSearchPopup_->setFont(popupFont);
1141 if (astSearchCompleter_ && astSearchCompleter_->popup()) {
1142 astSearchCompleter_->popup()->setFont(popupFont);
1145 astSearchPopup_->hide();
1148void MainWindow::setupTuSearch() {
1149 tuSearch_ =
new QLineEdit(
this);
1150 tuSearch_->setPlaceholderText(tr(
"Filter files..."));
1151 tuSearch_->setClearButtonEnabled(
true);
1152 tuSearch_->setProperty(
"searchField",
true);
1153 tuSearch_->setMinimumHeight(28);
1154 connect(tuSearch_, &QLineEdit::textChanged,
this,
1155 [
this](
const QString &text) {
1156 QString needle = text.trimmed();
1160 tuView_->setUpdatesEnabled(
false);
1163 std::function<bool(
const QModelIndex &)> filterRecursive =
1164 [&](
const QModelIndex &parent) ->
bool {
1165 bool anyChildVisible =
false;
1166 int rowCount = tuModel_->rowCount(parent);
1168 for (
int i = 0; i < rowCount; ++i) {
1169 QModelIndex idx = tuModel_->index(i, 0, parent);
1170 QString name = tuModel_->data(idx, Qt::DisplayRole).toString();
1173 bool matches = needle.isEmpty() ||
1174 name.contains(needle, Qt::CaseInsensitive);
1177 bool childVisible = filterRecursive(idx);
1180 bool visible = matches || childVisible;
1181 tuView_->setRowHidden(i, parent, !visible);
1184 anyChildVisible =
true;
1188 return anyChildVisible;
1191 filterRecursive(QModelIndex());
1193 tuView_->setUpdatesEnabled(
true);
1197 QWidget *container =
new QWidget(tuDock_);
1198 QVBoxLayout *layout =
new QVBoxLayout(container);
1199 layout->setContentsMargins(4, 4, 4, 4);
1200 layout->setSpacing(4);
1201 layout->addWidget(tuSearch_);
1202 layout->addWidget(tuView_);
1203 tuDock_->setWidget(container);
1206void MainWindow::triggerSourceSearch(
bool forward) {
1207 if (!sourceView_ || !sourceSearchInput_) {
1211 const QString term = sourceSearchInput_->text();
1212 if (term.trimmed().isEmpty()) {
1213 if (sourceSearchStatus_) {
1214 sourceSearchStatus_->setText(tr(
"Enter text"));
1220 forward ? sourceView_->findNext(term) : sourceView_->findPrevious(term);
1221 if (sourceSearchStatus_) {
1222 sourceSearchStatus_->setText(found ? QString() : tr(
"No matches"));
1226void MainWindow::onSourceSearchTextChanged(
const QString &text) {
1227 const bool hasText = !text.trimmed().isEmpty();
1230 sourceSearchPrevButton_->setEnabled(hasText);
1231 sourceSearchNextButton_->setEnabled(hasText);
1232 sourceView_->clearSearchHighlight();
1233 sourceSearchStatus_->clear();
1236 sourceSearchDebounce_->start();
1238 sourceSearchDebounce_->stop();
1242void MainWindow::onSourceSearchDebounced() { triggerSourceSearch(
true); }
1244void MainWindow::onSourceSearchFindNext() {
1245 if (sourceSearchDebounce_) {
1246 sourceSearchDebounce_->stop();
1248 triggerSourceSearch(
true);
1251void MainWindow::onSourceSearchFindPrevious() {
1252 if (sourceSearchDebounce_) {
1253 sourceSearchDebounce_->stop();
1255 triggerSourceSearch(
false);
1258void MainWindow::onAstSearchTextChanged(
const QString &text) {
1259 const bool hasText = !text.trimmed().isEmpty();
1261 astSearchPrevButton_->setEnabled(hasText);
1262 astSearchNextButton_->setEnabled(hasText);
1263 clearAstSearchState();
1266void MainWindow::onAstSearchDebounced() { triggerAstSearch(
true); }
1268void MainWindow::onAstSearchFindNext() { triggerAstSearch(
true); }
1270void MainWindow::onAstSearchFindPrevious() { triggerAstSearch(
false); }
1272void MainWindow::triggerAstSearch(
bool forward) {
1273 if (!astSearchInput_) {
1277 const QString expression = astSearchInput_->text().trimmed();
1278 if (expression.isEmpty()) {
1281 rememberAstSearchQuery(expression);
1283 if (!astModel_ || !astModel_->hasNodes()) {
1284 setAstSearchStatus(tr(
"No AST loaded"),
true);
1288 if (astSearchMatches_.empty()) {
1289 collectAstSearchMatches(expression);
1291 if (astSearchMatches_.empty()) {
1292 if (astSearchStatus_ &&
1293 astSearchStatus_->text() != tr(
"Invalid pattern")) {
1294 setAstSearchStatus(tr(
"No matches"),
false);
1299 astSearchCurrentIndex_ =
1300 forward ? 0 :
static_cast<int>(astSearchMatches_.size()) - 1;
1302 int count =
static_cast<int>(astSearchMatches_.size());
1304 astSearchCurrentIndex_ = (astSearchCurrentIndex_ + 1) % count;
1306 astSearchCurrentIndex_ = (astSearchCurrentIndex_ - 1 + count) % count;
1310 navigateToAstMatch(astSearchCurrentIndex_);
1311 setAstSearchStatus(QString(
"%1 of %2")
1312 .arg(astSearchCurrentIndex_ + 1)
1313 .arg(astSearchMatches_.size()),
1317void MainWindow::collectAstSearchMatches(
const QString &expression) {
1318 astSearchMatches_.clear();
1319 astSearchCurrentIndex_ = -1;
1321 struct AstSearchCondition {
1323 QRegularExpression regex;
1327 std::vector<AstSearchCondition> conditions;
1328 QRegularExpression qualifierPattern(QStringLiteral(
"(\\w+):"));
1329 auto it = qualifierPattern.globalMatch(expression);
1332 struct QualifierPos {
1337 std::vector<QualifierPos> qualifiers;
1338 while (it.hasNext()) {
1339 auto match = it.next();
1340 qualifiers.push_back({
static_cast<int>(match.capturedStart()),
1341 static_cast<int>(match.capturedEnd()),
1342 match.captured(1)});
1345 if (qualifiers.empty()) {
1347 QRegularExpression regex(expression,
1348 QRegularExpression::CaseInsensitiveOption);
1349 if (!regex.isValid()) {
1350 setAstSearchStatus(tr(
"Invalid pattern"),
true);
1353 conditions.push_back({QString(), std::move(regex)});
1356 if (qualifiers.front().start > 0) {
1357 QString bareText = expression.left(qualifiers.front().start).trimmed();
1358 if (!bareText.isEmpty()) {
1359 QRegularExpression regex(bareText,
1360 QRegularExpression::CaseInsensitiveOption);
1361 if (!regex.isValid()) {
1362 setAstSearchStatus(tr(
"Invalid pattern"),
true);
1365 conditions.push_back({QString(), std::move(regex)});
1370 for (std::size_t i = 0; i < qualifiers.size(); ++i) {
1371 int valueStart = qualifiers[i].valueStart;
1372 int valueEnd = (i + 1 < qualifiers.size()) ? qualifiers[i + 1].start
1373 : expression.size();
1375 expression.mid(valueStart, valueEnd - valueStart).trimmed();
1379 value.isEmpty() ? value : QStringLiteral(
"^(?:%1)$").arg(value);
1380 QRegularExpression regex(anchored,
1381 QRegularExpression::CaseInsensitiveOption);
1382 if (!regex.isValid()) {
1383 setAstSearchStatus(tr(
"Invalid pattern"),
true);
1386 conditions.push_back({qualifiers[i].key, std::move(regex)});
1391 QString projectRoot = tuModel_ ? tuModel_->projectRoot() : QString();
1392 bool projectFilterActive =
1393 astSearchProjectFilter_ && astSearchProjectFilter_->isChecked();
1394 std::unordered_map<FileID, bool> projectFileCache;
1397 AstViewNode *root = astModel_->selectedNode();
1400 QModelIndex rootIndex = astModel_->index(0, 0);
1401 if (!rootIndex.isValid()) {
1404 root =
static_cast<AstViewNode *
>(
1405 rootIndex.data(AstModel::NodePtrRole).value<void *>());
1414 std::vector<AstViewNode *> stack;
1415 int topLevelCount = astModel_->rowCount();
1416 for (
int i = topLevelCount - 1; i >= 0; --i) {
1417 QModelIndex idx = astModel_->index(i, 0);
1418 if (idx.isValid()) {
1419 auto *node =
static_cast<AstViewNode *
>(
1420 idx.data(AstModel::NodePtrRole).value<void *>());
1422 stack.push_back(node);
1427 auto jsonValueToString = [](
const AcavJson &value) -> QString {
1428 if (value.is_boolean()) {
1429 return value.get<
bool>() ? QStringLiteral(
"true")
1430 : QStringLiteral(
"false");
1432 if (value.is_number_integer()) {
1433 return QString::number(value.get<int64_t>());
1435 if (value.is_number_unsigned()) {
1436 return QString::number(value.get<uint64_t>());
1438 if (value.is_number_float()) {
1439 return QString::number(value.get<
double>());
1441 if (value.is_string()) {
1442 return QString::fromStdString(value.get<InternedString>().str());
1447 while (!stack.empty()) {
1448 AstViewNode *node = stack.back();
1452 if (projectFilterActive) {
1453 FileID fileId = node->getSourceRange().begin().fileID();
1454 auto cacheIt = projectFileCache.find(fileId);
1455 if (cacheIt == projectFileCache.end()) {
1456 bool isProject =
false;
1457 if (fileId != FileManager::InvalidFileID && !projectRoot.isEmpty()) {
1458 std::string_view path = fileManager_.getFilePath(fileId);
1459 if (!path.empty()) {
1461 QString::fromUtf8(path.data(),
static_cast<int>(path.size()));
1462 isProject = (qpath == projectRoot) ||
1463 qpath.startsWith(projectRoot + QChar(
'/'));
1466 cacheIt = projectFileCache.emplace(fileId, isProject).first;
1468 if (!cacheIt->second) {
1470 const auto &children = node->getChildren();
1471 for (
auto childIt = children.rbegin(); childIt != children.rend();
1474 stack.push_back(*childIt);
1480 const auto &props = node->getProperties();
1481 bool allMatch =
true;
1483 for (
const auto &cond : conditions) {
1484 if (cond.key.isEmpty()) {
1486 bool anyPropMatch =
false;
1487 if (props.is_object()) {
1488 for (
auto it = props.begin(); it != props.end(); ++it) {
1489 QString val = jsonValueToString(*it);
1490 if (!val.isEmpty() && cond.regex.match(val).hasMatch()) {
1491 anyPropMatch =
true;
1496 if (!anyPropMatch) {
1502 QByteArray keyUtf8 = cond.key.toUtf8();
1503 const char *keyStr = keyUtf8.constData();
1504 auto propIt = props.find(keyStr);
1505 if (propIt == props.end()) {
1509 QString val = jsonValueToString(*propIt);
1510 if (!cond.regex.match(val).hasMatch()) {
1518 astSearchMatches_.push_back(node);
1522 const auto &children = node->getChildren();
1523 for (
auto childIt = children.rbegin(); childIt != children.rend();
1526 stack.push_back(*childIt);
1532void MainWindow::navigateToAstMatch(
int index) {
1533 if (index < 0 || index >=
static_cast<int>(astSearchMatches_.size())) {
1536 AstViewNode *node = astSearchMatches_[index];
1537 QModelIndex modelIndex = astModel_->selectNode(node);
1538 astView_->setCurrentIndex(modelIndex);
1539 astView_->scrollTo(modelIndex);
1542void MainWindow::clearAstSearchState() {
1543 astSearchMatches_.clear();
1544 astSearchCurrentIndex_ = -1;
1545 if (astSearchStatus_) {
1546 astSearchStatus_->clear();
1547 astSearchStatus_->setProperty(
"searchStatus",
true);
1548 astSearchStatus_->style()->unpolish(astSearchStatus_);
1549 astSearchStatus_->style()->polish(astSearchStatus_);
1553void MainWindow::setAstSearchStatus(
const QString &text,
bool isError) {
1554 if (!astSearchStatus_) {
1557 astSearchStatus_->setText(text);
1563 astSearchStatus_->setProperty(
"searchStatus", QStringLiteral(
"error"));
1565 astSearchStatus_->setProperty(
"searchStatus",
true);
1567 astSearchStatus_->style()->unpolish(astSearchStatus_);
1568 astSearchStatus_->style()->polish(astSearchStatus_);
1571void MainWindow::showAstSearchPopup(
bool selectAll) {
1572 if (!astSearchPopup_ || !astSearchInput_) {
1576 syncAstSearchPopupGeometry();
1577 astSearchPopup_->show();
1579 astSearchPopup_->raise();
1580 astSearchPopup_->activateWindow();
1581 astSearchInput_->setFocus();
1583 astSearchInput_->selectAll();
1587void MainWindow::syncAstSearchPopupGeometry() {
1588 if (!astSearchPopup_) {
1592 const int minWidth = astSearchPopup_->minimumWidth();
1593 const int maxWidth = 900;
1594 int targetWidth = astSearchPopup_->width();
1595 QPoint anchor(0, 0);
1598 const int availableWidth = qMax(minWidth, astDock_->width() - 16);
1599 const int preferredWidth =
static_cast<int>(availableWidth * 0.85);
1600 targetWidth = qBound(minWidth, preferredWidth, qMin(maxWidth, availableWidth));
1601 const int x = qMax(8, astDock_->width() - targetWidth - 8);
1602 anchor = astDock_->mapToGlobal(QPoint(x, 40));
1604 const int availableWidth = qMax(minWidth, width() - 32);
1605 const int preferredWidth =
static_cast<int>(availableWidth * 0.55);
1606 targetWidth = qBound(minWidth, preferredWidth, qMin(maxWidth, availableWidth));
1607 anchor = mapToGlobal(QPoint(qMax(8, width() - targetWidth - 16), 40));
1610 int targetHeight = qMax(astSearchPopup_->sizeHint().height(),
1611 astSearchPopup_->minimumHeight());
1612 astSearchPopup_->resize(targetWidth, targetHeight);
1613 astSearchPopup_->move(anchor);
1616void MainWindow::rememberAstSearchQuery(
const QString &query) {
1617 const QString trimmed = query.trimmed();
1618 if (trimmed.isEmpty()) {
1622 astSearchHistory_.removeAll(trimmed);
1623 astSearchHistory_.prepend(trimmed);
1625 constexpr int kMaxQueryHistory = 20;
1626 while (astSearchHistory_.size() > kMaxQueryHistory) {
1627 astSearchHistory_.removeLast();
1630 if (astSearchHistoryModel_) {
1631 astSearchHistoryModel_->setStringList(astSearchHistory_);
1635void MainWindow::setAstCompilationWarningVisible(
bool visible) {
1636 if (!astCompilationWarningLabel_) {
1639 astCompilationWarningLabel_->setVisible(visible);
1642void MainWindow::applyFontSize(
int size) {
1644 if (size < kMinFontSize) {
1645 size = kMinFontSize;
1646 }
else if (size > kMaxFontSize) {
1647 size = kMaxFontSize;
1649 currentFontSize_ = size;
1651 sourceFontSize_ = size;
1652 astFontSize_ = size;
1653 declContextFontSize_ = size;
1654 logFontSize_ = size;
1655 currentFontFamily_ = AppConfig::instance().getFontFamily();
1657 QFont baseFont = QApplication::font();
1658 if (!currentFontFamily_.isEmpty()) {
1659 baseFont.setFamily(currentFontFamily_);
1661 baseFont.setPointSize(size);
1663 auto applyFont = [&baseFont](QWidget *widget) {
1667 widget->setFont(baseFont);
1671 applyFont(astView_);
1672 if (declContextView_) {
1673 declContextView_->applyFont(baseFont);
1675 applyFont(logDock_);
1676 applyFont(nodeCycleWidget_);
1677 applyFont(astSearchQuickInput_);
1678 applyFont(astSearchPopup_);
1679 if (astSearchCompleter_ && astSearchCompleter_->popup()) {
1680 astSearchCompleter_->popup()->setFont(baseFont);
1683 sourceView_->setFont(baseFont);
1684 sourceView_->applyFontSize(size);
1688void MainWindow::adjustFontSize(
int delta) {
1690 currentFontFamily_ = AppConfig::instance().getFontFamily();
1691 QFont baseFont = QApplication::font();
1692 if (!currentFontFamily_.isEmpty()) {
1693 baseFont.setFamily(currentFontFamily_);
1696 auto adjustWidget = [&](QWidget *widget,
int *fontSize) {
1697 if (!widget || !fontSize) {
1700 int nextSize = *fontSize + delta;
1701 if (nextSize < kMinFontSize) {
1702 nextSize = kMinFontSize;
1703 }
else if (nextSize > kMaxFontSize) {
1704 nextSize = kMaxFontSize;
1706 if (nextSize == *fontSize) {
1709 *fontSize = nextSize;
1710 baseFont.setPointSize(nextSize);
1711 widget->setFont(baseFont);
1714 if (focusedDock_ == tuDock_) {
1715 adjustWidget(tuView_, &tuFontSize_);
1716 }
else if (focusedDock_ == sourceDock_) {
1717 int nextSize = sourceFontSize_ + delta;
1718 if (nextSize < kMinFontSize) {
1719 nextSize = kMinFontSize;
1720 }
else if (nextSize > kMaxFontSize) {
1721 nextSize = kMaxFontSize;
1723 if (nextSize != sourceFontSize_ && sourceView_) {
1724 sourceFontSize_ = nextSize;
1725 baseFont.setPointSize(nextSize);
1726 sourceView_->setFont(baseFont);
1727 sourceView_->applyFontSize(nextSize);
1729 }
else if (focusedDock_ == astDock_) {
1730 adjustWidget(astView_, &astFontSize_);
1731 baseFont.setPointSize(astFontSize_);
1732 if (astSearchQuickInput_) {
1733 astSearchQuickInput_->setFont(baseFont);
1735 if (astSearchPopup_) {
1736 astSearchPopup_->setFont(baseFont);
1738 if (astSearchCompleter_ && astSearchCompleter_->popup()) {
1739 astSearchCompleter_->popup()->setFont(baseFont);
1741 }
else if (focusedDock_ == declContextDock_) {
1742 int nextSize = declContextFontSize_ + delta;
1743 if (nextSize < kMinFontSize) {
1744 nextSize = kMinFontSize;
1745 }
else if (nextSize > kMaxFontSize) {
1746 nextSize = kMaxFontSize;
1748 if (nextSize != declContextFontSize_ && declContextView_) {
1749 declContextFontSize_ = nextSize;
1750 baseFont.setPointSize(nextSize);
1751 declContextView_->applyFont(baseFont);
1753 }
else if (focusedDock_ == logDock_) {
1754 adjustWidget(logDock_, &logFontSize_);
1757 int nextSize = currentFontSize_ + delta;
1758 if (nextSize < kMinFontSize) {
1759 nextSize = kMinFontSize;
1760 }
else if (nextSize > kMaxFontSize) {
1761 nextSize = kMaxFontSize;
1763 if (nextSize != currentFontSize_) {
1764 applyFontSize(nextSize);
1769void MainWindow::expandFileExplorerTopLevel() {
1770 if (!tuView_ || !tuModel_) {
1775 int topLevelCount = tuModel_->rowCount();
1776 for (
int i = 0; i < topLevelCount; ++i) {
1777 QModelIndex topLevelIndex = tuModel_->index(i, 0);
1778 tuView_->expand(topLevelIndex);
1782void MainWindow::setupModels() {
1784 tuModel_ =
new TranslationUnitModel(fileManager_,
this);
1785 astModel_ =
new AstModel(
this);
1788 tuView_->setModel(tuModel_);
1789 astView_->setModel(astModel_);
1792 auto configureTreeView = [](QTreeView *view) {
1793 view->setHeaderHidden(
true);
1794 view->setAnimated(
false);
1795 view->setUniformRowHeights(
true);
1796 view->setTextElideMode(Qt::ElideNone);
1797 view->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
1798 view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
1799 view->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
1800 view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
1801 view->header()->setStretchLastSection(
false);
1805 view->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
1807 configureTreeView(tuView_);
1808 configureTreeView(astView_);
1811 queryRunner_ =
new QueryDependenciesRunner(
this);
1812 parallelQueryRunner_ =
new QueryDependenciesParallelRunner(
this);
1815 parallelQueryRunner_->setParallelCount(
1816 AppConfig::instance().getParallelProcessorCount());
1819 makeAstRunner_ =
new MakeAstRunner(
this);
1823 if (!clangResourceDir.empty()) {
1824 QString dir = QString::fromStdString(clangResourceDir);
1825 queryRunner_->setClangResourceDir(dir);
1826 parallelQueryRunner_->setClangResourceDir(dir);
1827 makeAstRunner_->setClangResourceDir(dir);
1831 astContext_ = std::make_unique<AstContext>();
1833 applyFontSize(AppConfig::instance().getFontSize());
1836 astWorkerThread_ =
new QThread(
this);
1837 astExtractorRunner_ =
new AstExtractorRunner(astContext_.get(), fileManager_);
1838 astExtractorRunner_->setCommentExtractionEnabled(
1839 AppConfig::instance().getCommentExtractionEnabled());
1842 nodeCycleWidget_ =
new NodeCycleWidget(
this);
1843 astExtractorRunner_->moveToThread(astWorkerThread_);
1844 astWorkerThread_->start();
1847void MainWindow::connectSignals() {
1849 connect(openAction_, &QAction::triggered,
this,
1850 &MainWindow::onOpenCompilationDatabase);
1851 connect(exitAction_, &QAction::triggered,
this, &MainWindow::onExit);
1854 connect(qApp, &QApplication::focusChanged,
this, &MainWindow::onFocusChanged);
1857 auto addFocusShortcut = [
this](
const QKeySequence &key,
auto callback) {
1858 auto *action =
new QAction(
this);
1859 action->setShortcut(key);
1860 connect(action, &QAction::triggered,
this, callback);
1864 addFocusShortcut(Qt::CTRL | Qt::Key_1, [
this]() { tuView_->setFocus(); });
1865 addFocusShortcut(Qt::CTRL | Qt::Key_2, [
this]() { sourceView_->setFocus(); });
1866 addFocusShortcut(Qt::CTRL | Qt::Key_3, [
this]() { astView_->setFocus(); });
1867 addFocusShortcut(Qt::CTRL | Qt::Key_4,
1868 [
this]() { declContextView_->focusSemanticTree(); });
1869 addFocusShortcut(Qt::CTRL | Qt::Key_5,
1870 [
this]() { declContextView_->focusLexicalTree(); });
1871 addFocusShortcut(Qt::CTRL | Qt::Key_6, [
this]() { logDock_->focusAllTab(); });
1872 addFocusShortcut(QKeySequence::Find, [
this]() {
1873 if (focusedDock_ == tuDock_ && tuSearch_) {
1874 tuSearch_->setFocus();
1875 tuSearch_->selectAll();
1876 }
else if (focusedDock_ == astDock_ && astSearchQuickInput_) {
1877 astSearchQuickInput_->setFocus();
1878 astSearchQuickInput_->selectAll();
1879 }
else if (sourceSearchInput_) {
1880 sourceSearchInput_->setFocus();
1881 sourceSearchInput_->selectAll();
1886 addFocusShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_E, [
this]() {
1887 if (astView_->hasFocus()) {
1888 expandAllChildren(astView_);
1889 }
else if (tuView_->hasFocus()) {
1890 onExpandAllTuChildren();
1893 addFocusShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_C, [
this]() {
1894 if (astView_->hasFocus()) {
1895 collapseAllChildren(astView_);
1896 }
else if (tuView_->hasFocus()) {
1897 onCollapseAllTuChildren();
1902 addFocusShortcut(Qt::Key_F5, [
this]() {
1903 QModelIndex index = tuView_->currentIndex();
1904 if (index.isValid()) {
1905 onTranslationUnitSelected(index);
1910 addFocusShortcut(Qt::CTRL | Qt::Key_I, [
this]() {
1911 QModelIndex index = astView_->currentIndex();
1912 if (!index.isValid()) {
1915 auto *node =
static_cast<AstViewNode *
>(index.internalPointer());
1917 onViewNodeDetails(node);
1923 if (tuView_->selectionModel()) {
1924 connect(tuView_->selectionModel(), &QItemSelectionModel::currentChanged,
1925 this, [
this](
const QModelIndex ¤t,
const QModelIndex &) {
1926 onTranslationUnitClicked(current);
1929 connect(tuView_, &QTreeView::doubleClicked,
this,
1930 &MainWindow::onTranslationUnitSelected);
1933 connect(astView_->selectionModel(), &QItemSelectionModel::currentChanged,
1934 this, &MainWindow::onAstNodeSelected);
1937 astView_->selectionModel(), &QItemSelectionModel::currentChanged,
this,
1938 [
this](
const QModelIndex ¤t,
const QModelIndex &) {
1939 if (isNavigatingFromDeclContext_) {
1942 if (!current.isValid()) {
1943 declContextView_->clear();
1946 auto *node =
static_cast<AstViewNode *
>(current.internalPointer());
1947 declContextView_->setSelectedNode(node);
1950 connect(declContextView_, &DeclContextView::contextNodeClicked,
this,
1951 [
this](AstViewNode *node) {
1955 isNavigatingFromDeclContext_ =
true;
1956 QModelIndex modelIndex = astModel_->selectNode(node);
1957 if (modelIndex.isValid()) {
1958 astView_->selectionModel()->setCurrentIndex(
1959 modelIndex, QItemSelectionModel::ClearAndSelect);
1960 astView_->scrollTo(modelIndex);
1962 isNavigatingFromDeclContext_ =
false;
1964 astView_->setContextMenuPolicy(Qt::CustomContextMenu);
1965 connect(astView_, &QTreeView::customContextMenuRequested,
this,
1966 &MainWindow::onAstContextMenuRequested);
1967 tuView_->setContextMenuPolicy(Qt::CustomContextMenu);
1968 connect(tuView_, &QTreeView::customContextMenuRequested,
this,
1969 &MainWindow::onTuContextMenuRequested);
1970 connect(sourceView_, &SourceCodeView::sourcePositionClicked,
this,
1971 &MainWindow::onSourcePositionClicked);
1972 connect(sourceView_, &SourceCodeView::sourceRangeSelected,
this,
1973 &MainWindow::onSourceRangeSelected);
1974 connect(nodeCycleWidget_, &NodeCycleWidget::nodeSelected,
this,
1975 &MainWindow::onCycleNodeSelected);
1976 connect(nodeCycleWidget_, &NodeCycleWidget::closed,
this,
1977 &MainWindow::onCycleWidgetClosed);
1980 connect(queryRunner_, &QueryDependenciesRunner::dependenciesReady,
this,
1981 &MainWindow::onDependenciesReady);
1982 connect(queryRunner_, &QueryDependenciesRunner::dependenciesReadyWithErrors,
1983 this, &MainWindow::onDependenciesReadyWithErrors);
1984 connect(queryRunner_, &QueryDependenciesRunner::error,
this,
1985 &MainWindow::onDependenciesError);
1986 connect(queryRunner_, &QueryDependenciesRunner::progress,
this,
1987 &MainWindow::onDependenciesProgress);
1989 connect(queryRunner_, &QueryDependenciesRunner::logMessage, logDock_,
1994 connect(parallelQueryRunner_,
1995 &QueryDependenciesParallelRunner::dependenciesReady,
this,
1996 &MainWindow::onDependenciesReady);
1997 connect(parallelQueryRunner_,
1998 &QueryDependenciesParallelRunner::dependenciesReadyWithErrors,
this,
1999 &MainWindow::onDependenciesReadyWithErrors);
2000 connect(parallelQueryRunner_, &QueryDependenciesParallelRunner::error,
this,
2001 &MainWindow::onDependenciesError);
2002 connect(parallelQueryRunner_, &QueryDependenciesParallelRunner::progress,
2003 this, &MainWindow::onDependenciesProgress);
2005 connect(parallelQueryRunner_, &QueryDependenciesParallelRunner::logMessage,
2006 logDock_, &LogDock::enqueue);
2010 connect(makeAstRunner_, &MakeAstRunner::astReady,
this,
2011 &MainWindow::onAstReady);
2012 connect(makeAstRunner_, &MakeAstRunner::error,
this, &MainWindow::onAstError);
2013 connect(makeAstRunner_, &MakeAstRunner::progress,
this,
2014 &MainWindow::onAstProgress);
2015 connect(makeAstRunner_, &MakeAstRunner::logMessage,
this,
2016 &MainWindow::onMakeAstLogMessage);
2018 connect(makeAstRunner_, &MakeAstRunner::logMessage, logDock_,
2023 connect(astExtractorRunner_, &AstExtractorRunner::finished,
this,
2024 &MainWindow::onAstExtracted);
2025 connect(astExtractorRunner_, &AstExtractorRunner::error,
this,
2026 &MainWindow::onAstError);
2027 connect(astExtractorRunner_, &AstExtractorRunner::progress,
this,
2028 &MainWindow::onAstProgress);
2029 connect(astExtractorRunner_, &AstExtractorRunner::statsUpdated,
this,
2030 &MainWindow::onAstStatsUpdated);
2031 connect(astExtractorRunner_, &AstExtractorRunner::started,
this,
2032 &MainWindow::onAstProgress);
2034 connect(astExtractorRunner_, &AstExtractorRunner::logMessage, logDock_,
2035 &LogDock::enqueue, Qt::QueuedConnection);
2040 const QString &projectRoot) {
2045 QFileInfo info(compilationDatabasePath);
2046 compilationDatabasePath_ = info.absoluteFilePath();
2047 const QString normalizedCompilationDatabasePath = compilationDatabasePath_;
2052 if (projectRoot.isEmpty()) {
2053 projectRoot_ = QString();
2055 projectRoot_ = QFileInfo(projectRoot).absoluteFilePath();
2060 QString outputFilePath =
2068 std::string errorMsg;
2069 std::vector<std::string> sourceFiles =
2071 normalizedCompilationDatabasePath.toStdString(), errorMsg);
2075 auto dedupSources = [](
const std::vector<std::string> &paths) {
2076 std::vector<std::string> unique;
2077 unique.reserve(paths.size());
2078 std::unordered_set<std::string> seen;
2079 for (
const std::string &path : paths) {
2080 if (seen.insert(path).second) {
2081 unique.push_back(path);
2086 sourceFiles = dedupSources(sourceFiles);
2088 if (sourceFiles.empty()) {
2089 QMessageBox::critical(
this, tr(
"Error"),
2090 tr(
"Failed to load source files: %1")
2091 .arg(QString::fromStdString(errorMsg)));
2096 (
static_cast<int>(sourceFiles.size()) >= kParallelThreshold);
2099 logStatus(LogLevel::Info,
2100 QString(
"Starting parallel dependency analysis (%1 files)...")
2101 .arg(sourceFiles.size()),
2102 QStringLiteral(
"query-dependencies"));
2103 parallelQueryRunner_->run(normalizedCompilationDatabasePath,
2106 logStatus(LogLevel::Info,
2107 QString(
"Loading compilation database: %1\nCache directory: %2")
2108 .arg(normalizedCompilationDatabasePath)
2110 QStringLiteral(
"query-dependencies"));
2111 queryRunner_->run(normalizedCompilationDatabasePath, outputFilePath);
2115void MainWindow::onOpenCompilationDatabase() {
2116 if (isAstExportInProgress_) {
2117 logStatus(LogLevel::Info, tr(
"AST export in progress, please wait..."));
2121 OpenProjectDialog dialog(
this);
2122 if (dialog.exec() != QDialog::Accepted) {
2126 QString dbPath = dialog.compilationDatabasePath();
2127 QString projectRoot = dialog.projectRootPath();
2129 if (dbPath.isEmpty()) {
2133 loadCompilationDatabase(dbPath, projectRoot);
2136void MainWindow::onExit() { close(); }
2138void MainWindow::onTranslationUnitClicked(
const QModelIndex &index) {
2139 if (!index.isValid()) {
2143 QString filePath = tuModel_->getSourceFilePathFromIndex(index);
2144 if (filePath.isEmpty()) {
2148 if (filePath == sourceView_->currentFilePath()) {
2152 logStatus(LogLevel::Info, QString(
"Loading file: %1").arg(filePath));
2154 if (sourceView_->loadFile(filePath)) {
2156 FileID fileId = fileManager_.registerFile(filePath.toStdString());
2157 sourceView_->setCurrentFileId(fileId);
2158 updateSourceSubtitle(filePath);
2160 logStatus(LogLevel::Info, QString(
"Loaded: %1").arg(filePath));
2165 bool shouldClearAst =
false;
2166 if (isSourceFile(filePath)) {
2168 shouldClearAst = (filePath != currentSourceFilePath_);
2169 }
else if (astContext_) {
2171 const auto &index = astContext_->getLocationIndex();
2172 shouldClearAst = !index.hasFile(fileId);
2175 if (shouldClearAst) {
2177 declContextView_->clear();
2178 astHasCompilationErrors_ =
false;
2179 setAstCompilationWarningVisible(
false);
2182 sourceView_->setCurrentFileId(FileManager::InvalidFileID);
2183 logStatus(LogLevel::Error, QString(
"Failed to load: %1").arg(filePath));
2188void MainWindow::onTranslationUnitSelected(
const QModelIndex &index) {
2189 if (!index.isValid()) {
2193 QString filePath = tuModel_->getSourceFilePathFromIndex(index);
2194 if (filePath.isEmpty()) {
2198 MemoryProfiler::checkpoint(
"File double-clicked - before checks");
2201 if (filePath == currentSourceFilePath_ && astModel_->hasNodes()) {
2202 logStatus(LogLevel::Info,
2203 QString(
"AST already loaded for: %1").arg(filePath));
2208 if (!isSourceFile(filePath)) {
2209 logStatus(LogLevel::Warning, tr(
"Cannot load AST for header files"));
2211 if (sourceView_->loadFile(filePath)) {
2212 FileID fileId = fileManager_.registerFile(filePath.toStdString());
2213 sourceView_->setCurrentFileId(fileId);
2214 updateSourceSubtitle(filePath);
2215 highlightTuFile(fileId);
2221 if (isAstExtractionInProgress_) {
2223 QFileInfo currentInfo(pendingSourceFilePath_);
2226 QString(
"AST extraction already in progress for %1. Please wait...")
2227 .arg(currentInfo.fileName()));
2231 if (isAstExportInProgress_) {
2232 logStatus(LogLevel::Info, tr(
"AST export in progress, please wait..."));
2236 MemoryProfiler::checkpoint(
"Before clearing old AST");
2240 astHasCompilationErrors_ =
false;
2241 setAstCompilationWarningVisible(
false);
2243 MemoryProfiler::checkpoint(
"After clearing old AST model");
2247 MemoryProfiler::checkpoint(
"After clearHistory()");
2251 if (astExtractorRunner_) {
2253 astExtractorRunner_->disconnect();
2255 astExtractorRunner_->moveToThread(QThread::currentThread());
2256 delete astExtractorRunner_;
2257 astExtractorRunner_ =
nullptr;
2260 astContext_.reset();
2262 MemoryProfiler::checkpoint(
"After destroying old AST context");
2264 astContext_ = std::make_unique<AstContext>();
2267 MemoryProfiler::checkpoint(
"After creating new AST context");
2270 astExtractorRunner_ =
new AstExtractorRunner(astContext_.get(), fileManager_);
2271 astExtractorRunner_->setCommentExtractionEnabled(
2272 AppConfig::instance().getCommentExtractionEnabled());
2273 astExtractorRunner_->moveToThread(astWorkerThread_);
2276 connect(astExtractorRunner_, &AstExtractorRunner::finished,
this,
2277 &MainWindow::onAstExtracted);
2278 connect(astExtractorRunner_, &AstExtractorRunner::error,
this,
2279 &MainWindow::onAstError);
2280 connect(astExtractorRunner_, &AstExtractorRunner::progress,
this,
2281 &MainWindow::onAstProgress);
2282 connect(astExtractorRunner_, &AstExtractorRunner::statsUpdated,
this,
2283 &MainWindow::onAstStatsUpdated);
2284 connect(astExtractorRunner_, &AstExtractorRunner::started,
this,
2285 &MainWindow::onAstProgress);
2287 logStatus(LogLevel::Info, QString(
"Loading file: %1").arg(filePath));
2289 if (sourceView_->loadFile(filePath)) {
2291 FileID fileId = fileManager_.registerFile(filePath.toStdString());
2292 sourceView_->setCurrentFileId(fileId);
2293 updateSourceSubtitle(filePath);
2294 logStatus(LogLevel::Info, QString(
"Loaded: %1").arg(filePath));
2296 sourceView_->setCurrentFileId(FileManager::InvalidFileID);
2297 logStatus(LogLevel::Error, QString(
"Failed to load: %1").arg(filePath));
2301 MemoryProfiler::checkpoint(
"After loading source file to view");
2304 AppConfig &config = AppConfig::instance();
2305 QString astFilePath =
2306 config.getAstFilePath(compilationDatabasePath_, filePath);
2308 QFileInfo astFileInfo(astFilePath);
2310 if (!astFileInfo.exists()) {
2312 currentSourceFilePath_ = filePath;
2313 pendingSourceFilePath_ = filePath;
2314 isAstExtractionInProgress_ =
true;
2315 astHasCompilationErrors_ =
false;
2316 QFileInfo sourceInfo(filePath);
2317 logStatus(LogLevel::Info,
2318 "Generating AST for " + sourceInfo.fileName() +
"...");
2319 onTimingMessage(QString(
"AST input files: %1 (source + headers)")
2320 .arg(getFileListForSource(filePath).size()));
2321 MemoryProfiler::checkpoint(
"Before make-ast generation");
2322 makeAstRunner_->run(compilationDatabasePath_, filePath, astFilePath);
2324 currentSourceFilePath_ = filePath;
2325 pendingSourceFilePath_ = filePath;
2326 isAstExtractionInProgress_ =
true;
2327 astHasCompilationErrors_ = loadAstCompilationErrorState(astFilePath);
2328 onTimingMessage(QString(
"AST input files: %1 (source + headers)")
2329 .arg(getFileListForSource(filePath).size()));
2330 MemoryProfiler::checkpoint(
"Before AST extraction from cache");
2332 extractAst(astFilePath, filePath);
2336void MainWindow::onDependenciesReady(
const QJsonObject &dependencies) {
2337 tuModel_->populateFromDependencies(dependencies, projectRoot_,
2338 compilationDatabasePath_);
2339 expandFileExplorerTopLevel();
2342 QJsonObject stats = dependencies[
"statistics"].toObject();
2343 int fileCount = stats[
"successCount"].toInt();
2344 int totalHeaders = stats[
"totalHeaderCount"].toInt();
2346 logStatus(LogLevel::Info,
2347 QString(
"Loaded %1 translation units").arg(fileCount),
2348 QStringLiteral(
"query-dependencies"));
2349 onTimingMessage(QString(
"Dependencies summary: %1 sources, %2 headers")
2351 .arg(totalHeaders));
2354void MainWindow::onDependenciesError(
const QString &errorMessage) {
2355 QMessageBox::critical(
this, tr(
"Error"), errorMessage);
2356 logStatus(LogLevel::Error, tr(
"Error loading dependencies"),
2357 QStringLiteral(
"query-dependencies"));
2360void MainWindow::onDependenciesProgress(
const QString &message) {
2361 logStatus(LogLevel::Info, message, QStringLiteral(
"query-dependencies"));
2364void MainWindow::onAstProgress(
const QString &message) {
2365 logStatus(LogLevel::Info, message, QStringLiteral(
"ast-extractor"));
2368void MainWindow::onAstStatsUpdated(
const AstExtractionStats &stats) {
2369 logStatus(LogLevel::Info,
2370 QString(
"Comments found: %1").arg(stats.commentCount),
2371 QStringLiteral(
"ast-extractor"));
2374void MainWindow::onDependenciesReadyWithErrors(
2375 const QJsonObject &dependencies,
const QStringList &errorMessages) {
2377 tuModel_->populateFromDependencies(dependencies, projectRoot_,
2378 compilationDatabasePath_);
2379 expandFileExplorerTopLevel();
2381 QJsonObject stats = dependencies[
"statistics"].toObject();
2382 int successCount = stats[
"successCount"].toInt();
2383 int failureCount = stats[
"failureCount"].toInt();
2384 int totalHeaders = stats[
"totalHeaderCount"].toInt();
2386 logStatus(LogLevel::Warning,
2387 QString(
"Loaded %1 translation units (%2 failed)")
2390 QStringLiteral(
"query-dependencies"));
2391 for (
const QString &errorMessage : errorMessages) {
2392 logStatus(LogLevel::Error, errorMessage,
2393 QStringLiteral(
"query-dependencies"));
2396 QString(
"Dependencies summary: %1 sources loaded, %2 failed, %3 headers")
2399 .arg(totalHeaders));
2402void MainWindow::logStatus(LogLevel level,
const QString &message,
2403 const QString &source) {
2404 const QString trimmed = message.trimmed();
2405 if (trimmed.isEmpty()) {
2410 entry.level = level;
2411 entry.source = source.isEmpty() ? QStringLiteral(
"acav") : source;
2412 entry.message = trimmed;
2413 entry.timestamp = QDateTime::currentDateTime();
2416 QMetaObject::invokeMethod(logDock_,
"enqueue", Qt::QueuedConnection,
2417 Q_ARG(LogEntry, entry));
2421void MainWindow::onAstReady(
const QString &astFilePath) {
2422 MemoryProfiler::checkpoint(
"After make-ast generation complete");
2423 persistAstCompilationErrorState(astFilePath, astHasCompilationErrors_);
2425 extractAst(astFilePath, currentSourceFilePath_);
2428void MainWindow::onAstExtracted(AstViewNode *root) {
2429 MemoryProfiler::checkpoint(
"AST extraction complete - before rendering");
2432 clearAstSearchState();
2433 if (astSearchInput_) {
2434 astSearchInput_->clear();
2438 isAstExtractionInProgress_ =
false;
2442 if (!currentSourceFilePath_.isEmpty()) {
2444 if (sourceView_->currentFilePath() != currentSourceFilePath_) {
2446 if (sourceView_->loadFile(currentSourceFilePath_)) {
2449 fileManager_.registerFile(currentSourceFilePath_.toStdString());
2450 sourceView_->setCurrentFileId(fileId);
2451 updateSourceSubtitle(currentSourceFilePath_);
2454 sourceView_->setCurrentFileId(FileManager::InvalidFileID);
2455 logStatus(LogLevel::Warning, QString(
"Failed to reload source file: %1")
2456 .arg(currentSourceFilePath_));
2464 FileID currentFileId = sourceView_->currentFileId();
2465 if (currentFileId != FileManager::InvalidFileID) {
2466 QModelIndex tuIndex = tuModel_->findIndexByFileId(currentFileId);
2467 if (tuIndex.isValid()) {
2468 tuView_->setCurrentIndex(tuIndex);
2469 tuView_->scrollTo(tuIndex);
2474 auto renderStart = std::chrono::steady_clock::now();
2476 MemoryProfiler::checkpoint(
"Before setting root node to model");
2478 std::size_t nodeCount = 0;
2480 nodeCount = astContext_->getAstViewNodeCount();
2481 astModel_->setTotalNodeCount(nodeCount);
2485 astModel_->setRootNode(root);
2486 updateAstSubtitle(currentSourceFilePath_);
2488 MemoryProfiler::checkpoint(
"After setting root node to model");
2490 auto renderEnd = std::chrono::steady_clock::now();
2491 std::chrono::duration<double> renderElapsed = renderEnd - renderStart;
2492 onTimingMessage(QString(
"render AST: %1s")
2493 .arg(QString::number(renderElapsed.count(),
'f', 2)));
2494 onTimingMessage(QString(
"AST nodes loaded: %1").arg(nodeCount));
2496 MemoryProfiler::checkpoint(
2497 QString(
"AST rendering complete (%1 nodes)").arg(nodeCount));
2499 const bool showCompilationWarning = astHasCompilationErrors_;
2500 setAstCompilationWarningVisible(showCompilationWarning);
2501 astHasCompilationErrors_ =
false;
2503 logStatus(LogLevel::Info, tr(
"AST loaded (%1 nodes)").arg(nodeCount));
2506void MainWindow::onAstError(
const QString &errorMessage) {
2508 isAstExtractionInProgress_ =
false;
2509 astHasCompilationErrors_ =
false;
2510 setAstCompilationWarningVisible(
false);
2512 QString targetAstPath = lastAstFilePath_;
2513 if (targetAstPath.isEmpty() && !compilationDatabasePath_.isEmpty() &&
2514 !currentSourceFilePath_.isEmpty()) {
2515 targetAstPath = AppConfig::instance().getAstFilePath(
2516 compilationDatabasePath_, currentSourceFilePath_);
2517 lastAstFilePath_ = targetAstPath;
2520 if (targetAstPath.isEmpty() || currentSourceFilePath_.isEmpty()) {
2521 logStatus(LogLevel::Error,
"Error: " + errorMessage);
2522 QMessageBox::critical(
this,
"AST Error",
2523 "Failed to generate or load AST:\n\n" + errorMessage);
2527 const bool cacheLoadFailed =
2528 errorMessage.contains(
"Failed to load AST from file",
2529 Qt::CaseInsensitive) ||
2530 errorMessage.contains(
"Failed to load AST", Qt::CaseInsensitive);
2531 if (!cacheLoadFailed) {
2532 logStatus(LogLevel::Error,
"Error: " + errorMessage);
2533 QMessageBox::critical(
this,
"AST Error",
2534 "Failed to generate or load AST:\n\n" + errorMessage);
2538 logStatus(LogLevel::Warning,
2539 tr(
"Cached AST load failed; regenerating automatically."));
2540 logStatus(LogLevel::Info, errorMessage);
2542 if (!deleteCachedAst(targetAstPath)) {
2543 logStatus(LogLevel::Error,
2544 tr(
"Failed to delete cached AST file: %1").arg(targetAstPath));
2545 QMessageBox::critical(
this,
"AST Error",
2546 "Failed to delete cached AST file:\n\n" +
2551 logStatus(LogLevel::Warning, tr(
"Regenerating AST after load failure..."));
2552 astHasCompilationErrors_ =
false;
2553 makeAstRunner_->run(compilationDatabasePath_, currentSourceFilePath_,
2557void MainWindow::onMakeAstLogMessage(
const LogEntry &entry) {
2558 if (entry.source == QStringLiteral(
"make-ast") &&
2559 entry.level == LogLevel::Error) {
2560 astHasCompilationErrors_ =
true;
2564void MainWindow::onAstContextMenuRequested(
const QPoint &pos) {
2565 QModelIndex index = astView_->indexAt(pos);
2566 if (!index.isValid()) {
2570 auto *node =
static_cast<AstViewNode *
>(index.internalPointer());
2576 const bool isTranslationUnit = (node->getParent() ==
nullptr);
2578 ::QMenu menu(astView_);
2580 SourceRange macroRange(SourceLocation(FileManager::InvalidFileID, 0, 0),
2581 SourceLocation(FileManager::InvalidFileID, 0, 0));
2582 const bool hasMacroRange = getMacroSpellingRange(node, ¯oRange);
2583 if (hasMacroRange) {
2584 QAction *macroAction = menu.addAction(tr(
"Go to Macro Definition"));
2585 connect(macroAction, &QAction::triggered,
this, [
this, node, macroRange]() {
2586 navigateToRange(macroRange, node,
false);
2588 menu.addSeparator();
2592 QAction *expandAllAction = menu.addAction(tr(
"Expand All"));
2593 QAction *collapseAllAction = menu.addAction(tr(
"Collapse All"));
2594 connect(expandAllAction, &QAction::triggered,
this,
2595 &MainWindow::onExpandAllAstChildren);
2596 connect(collapseAllAction, &QAction::triggered,
this,
2597 &MainWindow::onCollapseAllAstChildren);
2600 QAction *viewDetailsAction = menu.addAction(tr(
"View Details..."));
2601 viewDetailsAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_I));
2602 connect(viewDetailsAction, &QAction::triggered,
this,
2603 [
this, node]() { onViewNodeDetails(node); });
2606 menu.addSeparator();
2607 QAction *exportAction = menu.addAction(tr(
"Export Subtree to JSON..."));
2608 exportAction->setEnabled(!isTranslationUnit && astModel_ &&
2609 astModel_->hasNodes());
2610 connect(exportAction, &QAction::triggered,
this,
2611 [
this, node]() { onExportAst(node); });
2613 menu.exec(astView_->viewport()->mapToGlobal(pos));
2616void MainWindow::onViewNodeDetails(AstViewNode *node) {
2621 const AcavJson &props = node->getProperties();
2622 QString kind = tr(
"Node");
2625 if (props.contains(
"kind") && props.at(
"kind").is_string()) {
2626 kind = QString::fromStdString(props.at(
"kind").get<InternedString>().str());
2628 if (props.contains(
"name") && props.at(
"name").is_string()) {
2629 name = QString::fromStdString(props.at(
"name").get<InternedString>().str());
2632 QString title = tr(
"Node Details - %1").arg(kind);
2633 if (!name.isEmpty()) {
2634 title += QStringLiteral(
" '%1'").arg(name);
2640 const SourceRange &range = node->getSourceRange();
2641 AcavJson sourceRangeJson = AcavJson::object();
2643 auto locationToJson = [
this](
const SourceLocation &loc) {
2645 obj[
"fileId"] =
static_cast<uint64_t
>(loc.fileID());
2646 obj[
"line"] =
static_cast<uint64_t
>(loc.line());
2647 obj[
"column"] =
static_cast<uint64_t
>(loc.column());
2648 std::string_view filePath = fileManager_.getFilePath(loc.fileID());
2649 if (!filePath.empty()) {
2650 obj[
"filePath"] = InternedString(std::string(filePath));
2655 sourceRangeJson[
"begin"] = locationToJson(range.begin());
2656 sourceRangeJson[
"end"] = locationToJson(range.end());
2657 propertiesCopy[
"sourceRange"] = std::move(sourceRangeJson);
2659 auto *dialog =
new NodeDetailsDialog(std::move(propertiesCopy), title,
this);
2660 dialog->setAttribute(Qt::WA_DeleteOnClose);
2663 dialog->activateWindow();
2666void MainWindow::onExportAst(AstViewNode *node) {
2670 if (isAstExtractionInProgress_) {
2671 logStatus(LogLevel::Info, tr(
"AST extraction in progress, please wait..."));
2674 if (isAstExportInProgress_) {
2675 logStatus(LogLevel::Info, tr(
"AST export already in progress..."));
2680 if (!currentSourceFilePath_.isEmpty()) {
2681 QFileInfo info(currentSourceFilePath_);
2682 defaultDir = info.absolutePath();
2684 defaultDir = QDir::homePath();
2688 defaultDir + QDir::separator() + buildDefaultExportFileName(node);
2690 QString targetPath =
2691 QFileDialog::getSaveFileName(
this, tr(
"Export AST Subtree"), suggested,
2692 tr(
"JSON Files (*.json);;All Files (*)"));
2693 if (targetPath.isEmpty()) {
2697 auto confirm = QMessageBox::question(
2698 this, tr(
"Export AST"),
2699 tr(
"Exporting this subtree can take some time.\n\nDo you want to "
2701 QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
2702 if (confirm != QMessageBox::Yes) {
2706 auto *progress =
new QProgressDialog(
this);
2707 progress->setAttribute(Qt::WA_DeleteOnClose);
2708 progress->setWindowTitle(tr(
"Exporting AST"));
2709 progress->setLabelText(tr(
"Exporting AST subtree to JSON in the background.\n"
2710 "You can close this window and continue working.\n"
2711 "You will be notified when the export finishes."));
2712 progress->setCancelButtonText(tr(
"Close"));
2713 progress->setRange(0, 0);
2714 progress->setWindowModality(Qt::NonModal);
2716 connect(progress, &QProgressDialog::canceled, progress,
2717 &QProgressDialog::close);
2719 isAstExportInProgress_ =
true;
2720 QPointer<MainWindow> self(
this);
2721 QPointer<QProgressDialog> progressPtr(progress);
2722 FileManager *fileManager = &fileManager_;
2723 AstViewNode *exportRoot = node;
2724 QString exportPath = targetPath;
2726 auto *thread =
new QThread(
this);
2727 astExportThread_ = thread;
2728 auto *worker =
new QObject();
2729 worker->moveToThread(thread);
2732 thread, &QThread::started, worker,
2733 [self, progressPtr, exportRoot, exportPath, fileManager, thread]() {
2734 QString errorMessage;
2736 AcavJson json = buildAstJsonTree(exportRoot, *fileManager);
2737 InternedString serialized = json.dump(2);
2738 const std::string &data = serialized.str();
2740 QFile outFile(exportPath);
2741 if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
2742 errorMessage = QObject::tr(
"Unable to open file for writing:\n%1")
2744 }
else if (outFile.write(data.c_str(),
2745 static_cast<qsizetype
>(data.size())) == -1) {
2746 errorMessage = QObject::tr(
"Failed to write data to file:\n%1")
2751 }
catch (
const std::exception &ex) {
2752 errorMessage = QObject::tr(
"Failed to serialize AST subtree:\n%1")
2757 QMetaObject::invokeMethod(
2759 [self, progressPtr, exportPath, errorMessage]() {
2763 self->isAstExportInProgress_ =
false;
2765 progressPtr->close();
2767 auto *notice =
new QMessageBox(self);
2768 notice->setAttribute(Qt::WA_DeleteOnClose);
2769 notice->setStandardButtons(QMessageBox::Ok);
2770 notice->setWindowModality(Qt::NonModal);
2771 if (!errorMessage.isEmpty()) {
2772 notice->setIcon(QMessageBox::Warning);
2773 notice->setWindowTitle(QObject::tr(
"Export Failed"));
2774 notice->setText(errorMessage);
2776 notice->setIcon(QMessageBox::Information);
2777 notice->setWindowTitle(QObject::tr(
"Export Complete"));
2778 notice->setText(QObject::tr(
"Exported AST subtree to:\n%1")
2780 self->logStatus(LogLevel::Info,
2781 QObject::tr(
"Exported AST subtree to %1")
2786 Qt::QueuedConnection);
2792 connect(thread, &QThread::finished, worker, &QObject::deleteLater);
2793 connect(thread, &QThread::finished, thread, &QObject::deleteLater);
2794 connect(thread, &QThread::finished,
this,
2795 [
this]() { astExportThread_ =
nullptr; });
2799QString MainWindow::buildDefaultExportFileName(AstViewNode *node)
const {
2801 return QStringLiteral(
"ast_subtree.json");
2804 const AcavJson &props = node->getProperties();
2805 auto getStr = [&](
const char *key) -> QString {
2806 if (props.contains(key) && props.at(key).is_string()) {
2807 return QString::fromStdString(props.at(key).get<InternedString>().str());
2812 QString kind = getStr(
"kind");
2816 for (
const char *key : {
"name",
"declName",
"memberName"}) {
2818 if (!name.isEmpty()) {
2823 QString base = name.isEmpty() ? kind : QString(
"%1_%2").arg(kind, name);
2824 if (base.isEmpty()) {
2825 base = QStringLiteral(
"ast_subtree");
2830 sanitized.reserve(base.size());
2831 for (QChar ch : base) {
2832 if (ch.isLetterOrNumber() || ch ==
'_' || ch ==
'-') {
2833 sanitized.append(ch);
2834 }
else if (!sanitized.endsWith(
'_')) {
2835 sanitized.append(
'_');
2839 if (sanitized.isEmpty()) {
2840 return QStringLiteral(
"ast_subtree.json");
2843 return sanitized +
".json";
2846void MainWindow::extractAst(
const QString &astFilePath,
2847 const QString &sourceFilePath) {
2848 MemoryProfiler::checkpoint(
"Starting extractAst - queuing to worker");
2851 QStringList fileList = getFileListForSource(sourceFilePath);
2852 lastAstFilePath_ = astFilePath;
2853 currentSourceFilePath_ = sourceFilePath;
2857 QString compDbPath = compilationDatabasePath_;
2858 QMetaObject::invokeMethod(
2859 astExtractorRunner_,
2860 [
this, astFilePath, fileList, compDbPath]() {
2861 astExtractorRunner_->run(astFilePath, fileList, compDbPath);
2863 Qt::QueuedConnection);
2867MainWindow::getFileListForSource(
const QString &sourceFilePath)
const {
2868 QStringList fileList;
2869 fileList.append(sourceFilePath);
2872 QStringList headers = tuModel_->getIncludedHeadersForSource(sourceFilePath);
2873 fileList.append(headers);
2878bool MainWindow::isSourceFile(
const QString &filePath)
const {
2879 static const QStringList sourceExtensions = {
2880 ".cpp",
".cc",
".cxx",
".c",
2883 for (
const QString &ext : sourceExtensions) {
2884 if (filePath.endsWith(ext, Qt::CaseInsensitive)) {
2891bool MainWindow::validateSourceLookup(FileID fileId) {
2892 if (isAstExtractionInProgress_) {
2893 QFileInfo currentInfo(pendingSourceFilePath_);
2894 logStatus(LogLevel::Info,
2895 tr(
"AST extraction in progress for %1. Please wait...")
2896 .arg(currentInfo.fileName()));
2899 if (!astModel_ || !astModel_->hasNodes()) {
2900 logStatus(LogLevel::Info,
2901 tr(
"No AST available (code not yet compiled). Double-click the "
2902 "file in File Explorer to generate an AST."));
2905 if (!currentSourceFilePath_.isEmpty() && sourceView_ &&
2906 isSourceFile(sourceView_->currentFilePath())) {
2907 FileID currentAstFileId = FileManager::InvalidFileID;
2909 fileManager_.tryGetFileId(currentSourceFilePath_.toStdString())) {
2910 currentAstFileId = *existing;
2912 if (currentAstFileId != FileManager::InvalidFileID &&
2913 currentAstFileId != fileId) {
2914 logStatus(LogLevel::Info,
2915 tr(
"No AST available for this file yet. Double-click it in "
2916 "File Explorer to generate an AST."));
2923void MainWindow::logNoNodeFound(FileID fileId,
const QString &fallbackMessage) {
2924 const auto &index = astContext_->getLocationIndex();
2925 if (!index.hasFile(fileId)) {
2926 logStatus(LogLevel::Info,
2927 tr(
"No AST data for this file in the current translation unit. "
2928 "If this is a header, it may not be included. Double-click a "
2929 "source file in File Explorer to load its AST."));
2931 logStatus(LogLevel::Info, fallbackMessage);
2935bool MainWindow::deleteCachedAst(
const QString &astFilePath) {
2936 QFile astFile(astFilePath);
2937 if (astFile.exists() && !astFile.remove()) {
2941 const QString statusPath = astCacheStatusFilePath(astFilePath);
2942 QFile statusFile(statusPath);
2943 if (statusFile.exists() && !statusFile.remove()) {
2944 logStatus(LogLevel::Warning,
2945 tr(
"Failed to delete AST cache status file: %1").arg(statusPath));
2950QString MainWindow::astCacheStatusFilePath(
const QString &astFilePath)
const {
2951 return astFilePath +
".status";
2954void MainWindow::persistAstCompilationErrorState(
const QString &astFilePath,
2955 bool hasCompilationErrors) {
2956 if (astFilePath.isEmpty()) {
2960 const QString statusPath = astCacheStatusFilePath(astFilePath);
2961 QFile statusFile(statusPath);
2962 if (!statusFile.open(QIODevice::WriteOnly | QIODevice::Truncate |
2964 logStatus(LogLevel::Warning,
2965 tr(
"Failed to write AST cache status file: %1").arg(statusPath));
2969 const QByteArray payload = hasCompilationErrors ? QByteArrayLiteral(
"1\n")
2970 : QByteArrayLiteral(
"0\n");
2971 if (statusFile.write(payload) != payload.size()) {
2972 logStatus(LogLevel::Warning,
2973 tr(
"Failed to persist AST cache status: %1").arg(statusPath));
2977bool MainWindow::loadAstCompilationErrorState(
const QString &astFilePath)
const {
2978 if (astFilePath.isEmpty()) {
2982 QFile statusFile(astCacheStatusFilePath(astFilePath));
2983 if (!statusFile.exists() ||
2984 !statusFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
2988 const QByteArray value = statusFile.readAll().trimmed().toLower();
2989 return value ==
"1" || value ==
"true";
2992void MainWindow::clearHistory() {
2998void MainWindow::recordHistory(FileID fileId,
unsigned line,
unsigned column,
2999 AstViewNode *node) {
3000 if (suppressHistory_ || fileId == FileManager::InvalidFileID) {
3004 NavEntry entry{fileId, line, column, node, astVersion_};
3005 if (!history_.empty()) {
3006 const NavEntry ¤t = history_[historyCursor_];
3007 if (current.fileId == entry.fileId && current.line == entry.line &&
3008 current.column == entry.column && current.node == entry.node &&
3009 current.astVersion == entry.astVersion) {
3015 if (historyCursor_ + 1 < history_.size()) {
3016 history_.erase(history_.begin() +
static_cast<long>(historyCursor_) + 1,
3020 history_.push_back(entry);
3021 historyCursor_ = history_.size() - 1;
3023 static constexpr std::size_t kMaxHistory = 500;
3024 if (history_.size() > kMaxHistory) {
3025 std::size_t toRemove = history_.size() - kMaxHistory;
3026 history_.erase(history_.begin(),
3027 history_.begin() +
static_cast<long>(toRemove));
3028 historyCursor_ = history_.size() - 1;
3034void MainWindow::navigateHistory(
int delta) {
3035 if (history_.empty()) {
3038 int newIndex =
static_cast<int>(historyCursor_) + delta;
3039 if (newIndex < 0 || newIndex >=
static_cast<int>(history_.size())) {
3042 historyCursor_ =
static_cast<std::size_t
>(newIndex);
3043 applyEntry(history_[historyCursor_]);
3047void MainWindow::applyEntry(
const NavEntry &entry) {
3048 if (entry.fileId == FileManager::InvalidFileID) {
3052 std::string_view path = fileManager_.getFilePath(entry.fileId);
3054 logStatus(LogLevel::Warning, tr(
"History target file is unavailable"));
3058 suppressHistory_ =
true;
3059 QString qPath = QString::fromStdString(std::string(path));
3061 if (sourceView_->currentFileId() != entry.fileId) {
3062 if (!sourceView_->loadFile(qPath)) {
3063 logStatus(LogLevel::Error, QString(
"Failed to load %1").arg(qPath));
3064 suppressHistory_ =
false;
3067 sourceView_->setCurrentFileId(entry.fileId);
3068 updateSourceSubtitle(qPath);
3071 highlightTuFile(entry.fileId);
3073 SourceLocation loc(entry.fileId, entry.line, entry.column);
3074 SourceRange range(loc, loc);
3075 sourceView_->highlightRange(range);
3077 if (entry.astVersion == astVersion_ && entry.node) {
3078 QModelIndex modelIndex = astModel_->selectNode(entry.node);
3079 if (modelIndex.isValid()) {
3080 astView_->selectionModel()->setCurrentIndex(
3081 modelIndex, QItemSelectionModel::ClearAndSelect);
3082 astView_->scrollTo(modelIndex);
3086 suppressHistory_ =
false;
3089void MainWindow::updateNavActions() {
3090 bool hasHistory = !history_.empty();
3091 if (navBackAction_) {
3092 navBackAction_->setEnabled(hasHistory && historyCursor_ > 0);
3093 if (hasHistory && historyCursor_ > 0) {
3094 const NavEntry &target = history_[historyCursor_ - 1];
3095 std::string_view path = fileManager_.getFilePath(target.fileId);
3096 navBackAction_->setToolTip(
3097 QString(
"Back to %1:%2")
3098 .arg(QString::fromStdString(std::string(path)))
3101 navBackAction_->setToolTip(tr(
"Back"));
3104 if (navForwardAction_) {
3105 navForwardAction_->setEnabled(hasHistory &&
3106 (historyCursor_ + 1 < history_.size()));
3107 if (hasHistory && historyCursor_ + 1 < history_.size()) {
3108 const NavEntry &target = history_[historyCursor_ + 1];
3109 std::string_view path = fileManager_.getFilePath(target.fileId);
3110 navForwardAction_->setToolTip(
3111 QString(
"Forward to %1:%2")
3112 .arg(QString::fromStdString(std::string(path)))
3115 navForwardAction_->setToolTip(tr(
"Forward"));
3120void MainWindow::onTimingMessage(
const QString &message) {
3121 QString trimmed = message.trimmed();
3122 if (trimmed.isEmpty()) {
3126 if (trimmed.startsWith(QStringLiteral(
"Timing "))) {
3129 logStatus(LogLevel::Debug, trimmed, QStringLiteral(
"acav-timing"));
3134void MainWindow::onAstNodeSelected(
const QModelIndex &index) {
3135 if (!index.isValid() || !astContext_) {
3139 AstViewNode *node =
static_cast<AstViewNode *
>(index.internalPointer());
3144 if (suppressSourceHighlight_) {
3145 suppressSourceHighlight_ =
false;
3146 astModel_->updateSelectionFromIndex(index);
3151 astModel_->updateSelectionFromIndex(index);
3153 const SourceRange &range = node->getSourceRange();
3154 if (goToMacroDefinitionAction_) {
3155 SourceRange macroRange(SourceLocation(FileManager::InvalidFileID, 0, 0),
3156 SourceLocation(FileManager::InvalidFileID, 0, 0));
3157 bool hasMacroRange = getMacroSpellingRange(node, ¯oRange);
3158 goToMacroDefinitionAction_->setEnabled(hasMacroRange);
3160 bool skipCursorMove = suppressSourceCursorMove_;
3161 suppressSourceCursorMove_ =
false;
3162 navigateToRange(range, node, skipCursorMove);
3165bool MainWindow::getMacroSpellingRange(
const AstViewNode *node,
3166 SourceRange *outRange)
const {
3167 if (!node || !outRange) {
3170 const auto &properties = node->getProperties();
3171 auto it = properties.find(
"macroSpellingRange");
3172 if (it == properties.end()) {
3175 return parseSourceRangeJson(*it, outRange);
3178bool MainWindow::navigateToRange(
const SourceRange &range, AstViewNode *node,
3179 bool skipCursorMove) {
3180 FileID rangeFileId = range.begin().fileID();
3183 if (rangeFileId == FileManager::InvalidFileID) {
3186 if (!currentSourceFilePath_.isEmpty()) {
3188 fileManager_.tryGetFileId(currentSourceFilePath_.toStdString())) {
3189 highlightTuFile(*fileId);
3193 sourceView_->clearHighlight();
3198 if (rangeFileId != sourceView_->currentFileId()) {
3200 std::string_view filePath = fileManager_.getFilePath(rangeFileId);
3201 if (filePath.empty()) {
3202 logStatus(LogLevel::Warning, tr(
"Cannot find file path for AST node"));
3207 QString qFilePath = QString::fromStdString(std::string(filePath));
3208 if (!sourceView_->loadFile(qFilePath)) {
3209 logStatus(LogLevel::Error,
3210 QString(
"Failed to load file: %1").arg(qFilePath));
3215 sourceView_->setCurrentFileId(rangeFileId);
3216 updateSourceSubtitle(qFilePath);
3220 sourceView_->highlightRange(range, !skipCursorMove);
3221 highlightTuFile(rangeFileId);
3224 recordHistory(rangeFileId, range.begin().line(), range.begin().column(),
3231void MainWindow::onGoToMacroDefinition() {
3232 if (!astContext_ || !astView_) {
3235 QModelIndex index = astView_->currentIndex();
3236 if (!index.isValid()) {
3239 AstViewNode *node =
static_cast<AstViewNode *
>(index.internalPointer());
3243 SourceRange macroRange(SourceLocation(FileManager::InvalidFileID, 0, 0),
3244 SourceLocation(FileManager::InvalidFileID, 0, 0));
3245 if (!getMacroSpellingRange(node, ¯oRange)) {
3246 logStatus(LogLevel::Info, tr(
"No macro definition for this AST node"));
3249 navigateToRange(macroRange, node,
false);
3252void MainWindow::onSourcePositionClicked(FileID fileId,
unsigned line,
3254 if (!astContext_ || fileId == FileManager::InvalidFileID) {
3257 if (!validateSourceLookup(fileId)) {
3261 const auto &index = astContext_->getLocationIndex();
3262 auto matches = index.getNodesAt(fileId, line, column);
3264 if (matches.empty()) {
3265 logNoNodeFound(fileId, tr(
"No AST node at this position"));
3276 auto getRangeSize = [](
const SourceRange &range) -> RangeSize {
3277 unsigned lines = range.end().line() - range.begin().line();
3278 unsigned columns = 0;
3281 columns = range.end().column() - range.begin().column();
3286 return {lines, columns};
3289 auto compareRangeSize = [](
const RangeSize &a,
const RangeSize &b) ->
int {
3290 if (a.lines != b.lines)
3291 return a.lines < b.lines ? -1 : 1;
3292 if (a.columns != b.columns)
3293 return a.columns < b.columns ? -1 : 1;
3298 RangeSize minSize = getRangeSize(matches[0]->getSourceRange());
3299 for (
auto *node : matches) {
3300 RangeSize size = getRangeSize(node->getSourceRange());
3301 if (compareRangeSize(size, minSize) < 0) {
3307 std::vector<AstViewNode *> mostSpecific;
3308 for (
auto *node : matches) {
3309 RangeSize size = getRangeSize(node->getSourceRange());
3310 if (compareRangeSize(size, minSize) == 0) {
3311 mostSpecific.push_back(node);
3316 AstViewNode *selected = mostSpecific.front();
3318 QModelIndex modelIndex = astModel_->selectNode(selected);
3319 if (modelIndex.isValid()) {
3320 suppressSourceCursorMove_ =
true;
3321 astView_->selectionModel()->setCurrentIndex(
3322 modelIndex, QItemSelectionModel::ClearAndSelect);
3323 astView_->scrollTo(modelIndex);
3324 suppressSourceCursorMove_ =
false;
3325 const SourceRange &range = selected->getSourceRange();
3326 recordHistory(range.begin().fileID(), range.begin().line(),
3327 range.begin().column(), selected);
3331void MainWindow::onSourceRangeSelected(FileID fileId,
unsigned startLine,
3332 unsigned startColumn,
unsigned endLine,
3333 unsigned endColumn) {
3334 if (!astContext_ || fileId == FileManager::InvalidFileID) {
3337 if (!validateSourceLookup(fileId)) {
3341 const auto &index = astContext_->getLocationIndex();
3342 AstViewNode *match = index.getFirstNodeContainedInRange(
3343 fileId, startLine, startColumn, endLine, endColumn);
3346 logNoNodeFound(fileId, tr(
"No AST node within selection"));
3350 QModelIndex modelIndex = astModel_->selectNode(match);
3351 if (modelIndex.isValid()) {
3352 suppressSourceHighlight_ =
true;
3353 astView_->selectionModel()->setCurrentIndex(
3354 modelIndex, QItemSelectionModel::ClearAndSelect);
3355 astView_->scrollTo(modelIndex);
3356 const SourceRange &range = match->getSourceRange();
3357 recordHistory(range.begin().fileID(), range.begin().line(),
3358 range.begin().column(), match);
3362void MainWindow::highlightTuFile(FileID fileId) {
3363 if (!tuModel_ || !tuView_) {
3367 QModelIndex tuIndex;
3369 if (fileId != FileManager::InvalidFileID) {
3370 tuIndex = tuModel_->findIndexByFileId(fileId);
3373 if (!tuIndex.isValid() && fileId != FileManager::InvalidFileID) {
3374 if (!currentSourceFilePath_.isEmpty()) {
3375 std::string_view path = fileManager_.getFilePath(fileId);
3376 if (!path.empty()) {
3377 QString qPath = QString::fromStdString(std::string(path));
3378 QModelIndex sourceRoot =
3379 tuModel_->findIndexByFilePath(currentSourceFilePath_);
3380 if (sourceRoot.isValid()) {
3381 if (tuModel_->canFetchMore(sourceRoot)) {
3382 tuModel_->fetchMore(sourceRoot);
3384 tuIndex = tuModel_->findIndexByAnyFilePathUnder(qPath, sourceRoot);
3390 if (!tuIndex.isValid()) {
3395 QModelIndex parent = tuIndex.parent();
3396 while (parent.isValid()) {
3397 tuView_->expand(parent);
3398 parent = parent.parent();
3402 tuView_->selectionModel()->setCurrentIndex(
3403 tuIndex, QItemSelectionModel::ClearAndSelect);
3404 tuView_->scrollTo(tuIndex, QAbstractItemView::PositionAtCenter);
3406 QRect rect = tuView_->visualRect(tuIndex);
3407 if (rect.isValid()) {
3409 QModelIndex p = tuIndex.parent();
3410 while (p.isValid()) {
3415 QString text = tuIndex.data(Qt::DisplayRole).toString();
3416 QFontMetrics fm(tuView_->font());
3417 int textWidth = fm.horizontalAdvance(text);
3420 QVariant iconVar = tuIndex.data(Qt::DecorationRole);
3421 if (iconVar.canConvert<QIcon>()) {
3422 QSize iconSize = tuView_->iconSize();
3423 if (iconSize.isValid()) {
3424 iconWidth = iconSize.width() + 4;
3428 int indent = tuView_->indentation() * depth;
3430 int textLeft = rect.left() + indent + iconWidth + padding;
3431 int textRight = textLeft + textWidth;
3433 QScrollBar *hBar = tuView_->horizontalScrollBar();
3434 int viewWidth = tuView_->viewport()->width();
3436 hBar->setValue(hBar->value() + textLeft);
3437 }
else if (textRight > viewWidth) {
3438 hBar->setValue(hBar->value() + (textRight - viewWidth));
3443void MainWindow::onCycleNodeSelected(AstViewNode *node) {
3448 QModelIndex modelIndex = astModel_->selectNode(node);
3449 if (modelIndex.isValid()) {
3450 astView_->selectionModel()->setCurrentIndex(
3451 modelIndex, QItemSelectionModel::ClearAndSelect);
3452 astView_->scrollTo(modelIndex);
3455 const SourceRange &range = node->getSourceRange();
3456 highlightTuFile(range.begin().fileID());
3457 if (range.begin().fileID() == sourceView_->currentFileId()) {
3458 sourceView_->highlightRange(range);
3460 recordHistory(range.begin().fileID(), range.begin().line(),
3461 range.begin().column(), node);
3465void MainWindow::onCycleWidgetClosed() {
3469void MainWindow::onExpandAllAstChildren() { expandAllChildren(astView_); }
3471void MainWindow::onCollapseAllAstChildren() { collapseAllChildren(astView_); }
3475bool isTuSourceNode(
const QModelIndex &index) {
3476 return index.data(Qt::UserRole + 3).toBool();
3480bool isSourceFileOrDescendant(
const QModelIndex &index) {
3481 QModelIndex current = index;
3482 while (current.isValid()) {
3483 if (isTuSourceNode(current)) {
3486 current = current.parent();
3493void MainWindow::onTuContextMenuRequested(
const QPoint &pos) {
3494 QModelIndex index = tuView_->indexAt(pos);
3495 if (!index.isValid()) {
3500 tuView_->setCurrentIndex(index);
3502 ::QMenu menu(tuView_);
3508 QAction *expandAction = menu.addAction(tr(
"Expand All"));
3509 QAction *collapseAction = menu.addAction(tr(
"Collapse All"));
3511 connect(expandAction, &QAction::triggered,
this,
3512 &MainWindow::onExpandAllTuChildren);
3513 connect(collapseAction, &QAction::triggered,
this,
3514 &MainWindow::onCollapseAllTuChildren);
3516 menu.exec(tuView_->viewport()->mapToGlobal(pos));
3519void MainWindow::onExpandAllTuChildren() {
3520 QModelIndex currentIndex = tuView_->currentIndex();
3521 if (!currentIndex.isValid()) {
3528 if (isSourceFileOrDescendant(currentIndex)) {
3529 expandSubtree(tuView_);
3531 expandAllChildren(tuView_);
3535void MainWindow::onCollapseAllTuChildren() {
3536 QModelIndex currentIndex = tuView_->currentIndex();
3537 if (!currentIndex.isValid()) {
3544 if (isSourceFileOrDescendant(currentIndex)) {
3545 collapseAllChildren(tuView_);
3547 collapseTuDirectories(tuView_);
3551void MainWindow::expandAllChildren(QTreeView *view) {
3552 QModelIndex currentIndex = view->currentIndex();
3553 if (!currentIndex.isValid()) {
3558 QHeaderView::ResizeMode prevResizeMode = view->header()->sectionResizeMode(0);
3559 view->header()->setSectionResizeMode(QHeaderView::Fixed);
3560 view->setUpdatesEnabled(
false);
3564 if (view->selectionModel()) {
3565 view->selectionModel()->blockSignals(
true);
3568 if (view == tuView_) {
3571 std::vector<QModelIndex> stack;
3573 stack.push_back(currentIndex);
3575 QAbstractItemModel *model = view->model();
3576 while (!stack.empty()) {
3577 QModelIndex idx = stack.back();
3579 if (!idx.isValid()) {
3583 if (isTuSourceNode(idx)) {
3587 int childCount = model->rowCount(idx);
3588 if (childCount <= 0) {
3592 if (!view->isExpanded(idx)) {
3596 for (
int row = 0; row < childCount; ++row) {
3597 stack.push_back(model->index(row, 0, idx));
3602 view->expandRecursively(currentIndex);
3605 if (view->selectionModel()) {
3606 view->selectionModel()->blockSignals(
false);
3608 view->setUpdatesEnabled(
true);
3609 view->header()->setSectionResizeMode(prevResizeMode);
3612void MainWindow::expandSubtree(QTreeView *view) {
3617 QModelIndex currentIndex = view->currentIndex();
3618 if (!currentIndex.isValid()) {
3622 QHeaderView::ResizeMode prevResizeMode = view->header()->sectionResizeMode(0);
3623 view->header()->setSectionResizeMode(QHeaderView::Fixed);
3624 view->setUpdatesEnabled(
false);
3625 if (view->selectionModel()) {
3626 view->selectionModel()->blockSignals(
true);
3629 QAbstractItemModel *model = view->model();
3630 std::vector<QModelIndex> stack;
3632 stack.push_back(currentIndex);
3634 while (!stack.empty()) {
3635 QModelIndex idx = stack.back();
3637 if (!idx.isValid()) {
3641 if (model->canFetchMore(idx)) {
3642 model->fetchMore(idx);
3645 int childCount = model->rowCount(idx);
3646 if (childCount > 0 && !view->isExpanded(idx)) {
3650 for (
int row = 0; row < childCount; ++row) {
3651 stack.push_back(model->index(row, 0, idx));
3655 if (view->selectionModel()) {
3656 view->selectionModel()->blockSignals(
false);
3658 view->setUpdatesEnabled(
true);
3659 view->header()->setSectionResizeMode(prevResizeMode);
3662void MainWindow::collapseAllChildren(QTreeView *view) {
3663 QModelIndex currentIndex = view->currentIndex();
3664 if (!currentIndex.isValid()) {
3668 QHeaderView::ResizeMode prevResizeMode = view->header()->sectionResizeMode(0);
3669 view->header()->setSectionResizeMode(QHeaderView::Fixed);
3670 view->setUpdatesEnabled(
false);
3674 if (view->selectionModel()) {
3675 view->selectionModel()->blockSignals(
true);
3678 if (!currentIndex.parent().isValid()) {
3681 view->collapseAll();
3684 collapseRecursively(currentIndex, view);
3687 if (view->selectionModel()) {
3688 view->selectionModel()->blockSignals(
false);
3690 view->setUpdatesEnabled(
true);
3691 view->header()->setSectionResizeMode(prevResizeMode);
3694void MainWindow::collapseTuDirectories(QTreeView *view) {
3699 QModelIndex currentIndex = view->currentIndex();
3700 if (!currentIndex.isValid()) {
3704 QHeaderView::ResizeMode prevResizeMode = view->header()->sectionResizeMode(0);
3705 view->header()->setSectionResizeMode(QHeaderView::Fixed);
3706 view->setUpdatesEnabled(
false);
3707 if (view->selectionModel()) {
3708 view->selectionModel()->blockSignals(
true);
3711 QAbstractItemModel *model = view->model();
3712 std::vector<QModelIndex> stack;
3713 std::vector<QModelIndex> postOrder;
3715 postOrder.reserve(256);
3716 stack.push_back(currentIndex);
3718 while (!stack.empty()) {
3719 QModelIndex idx = stack.back();
3721 if (!idx.isValid()) {
3725 postOrder.push_back(idx);
3727 int rowCount = model->rowCount(idx);
3728 for (
int row = 0; row < rowCount; ++row) {
3729 QModelIndex child = model->index(row, 0, idx);
3732 if (view->isExpanded(child)) {
3733 stack.push_back(child);
3738 for (std::size_t i = postOrder.size(); i-- > 0;) {
3739 view->collapse(postOrder[i]);
3742 if (view->selectionModel()) {
3743 view->selectionModel()->blockSignals(
false);
3746 view->setUpdatesEnabled(
true);
3747 view->header()->setSectionResizeMode(prevResizeMode);
3750void MainWindow::collapseRecursively(
const QModelIndex &index,
3752 if (!index.isValid() || !view) {
3756 QAbstractItemModel *model = view->model();
3757 std::vector<QModelIndex> stack;
3758 std::vector<QModelIndex> postOrder;
3760 postOrder.reserve(256);
3761 stack.push_back(index);
3763 while (!stack.empty()) {
3764 QModelIndex idx = stack.back();
3766 if (!idx.isValid()) {
3770 postOrder.push_back(idx);
3772 int rowCount = model->rowCount(idx);
3773 for (
int row = 0; row < rowCount; ++row) {
3774 QModelIndex child = model->index(row, 0, idx);
3775 if (view->isExpanded(child)) {
3776 stack.push_back(child);
3781 for (std::size_t i = postOrder.size(); i-- > 0;) {
3782 view->collapse(postOrder[i]);
3786void MainWindow::updateSourceSubtitle(
const QString &filePath) {
3787 if (!sourceTitleBar_ || filePath.isEmpty()) {
3788 if (sourceTitleBar_) {
3789 sourceTitleBar_->setSubtitle(QString());
3794 QString projectRoot = tuModel_ ? tuModel_->projectRoot() : QString();
3797 if (!projectRoot.isEmpty() && filePath.startsWith(projectRoot +
"/")) {
3799 QString relativePath = filePath.mid(projectRoot.length() + 1);
3800 subtitle = QStringLiteral(
"[project] %1").arg(relativePath);
3803 subtitle = QStringLiteral(
"[external] %1").arg(filePath);
3806 sourceTitleBar_->setSubtitle(subtitle);
3809void MainWindow::updateAstSubtitle(
const QString &mainSourcePath) {
3810 if (!astTitleBar_ || mainSourcePath.isEmpty()) {
3812 astTitleBar_->setSubtitle(QString());
3817 QString projectRoot = tuModel_ ? tuModel_->projectRoot() : QString();
3820 if (!projectRoot.isEmpty() && mainSourcePath.startsWith(projectRoot +
"/")) {
3822 QString relativePath = mainSourcePath.mid(projectRoot.length() + 1);
3823 subtitle = QStringLiteral(
"[project] %1").arg(relativePath);
3826 subtitle = QStringLiteral(
"[external] %1").arg(mainSourcePath);
3829 astTitleBar_->setSubtitle(subtitle);
Application configuration and settings.
nlohmann::basic_json< std::map, std::vector, InternedString, bool, int64_t, uint64_t, double, std::allocator, nlohmann::adl_serializer, std::vector< uint8_t > > AcavJson
Custom JSON type using InternedString for automatic string deduplication.
Utilities for interacting with Clang at runtime This includes runtime detection of Clang paths and AS...
std::vector< std::string > getSourceFilesFromCompilationDatabase(const std::string &compDbPath, std::string &errorMessage)
Extract source file paths from a compilation database.
std::string getClangResourceDir(const std::string &overrideResourceDir="")
Get clang resource directory.
std::size_t FileID
Type-safe identifier for registered files. 0 is reserved for invalid.
Memory-efficient immutable string with automatic deduplication.
Main application window with multi-panel layout.
Cross-platform memory profiling utility.
Non-modal dialog displaying AST node properties in a tree view.
Dialog for opening a project with compilation database and optional project root.
Source code location representation.
Application configuration manager.
QString getDependenciesFilePath(const QString &compilationDatabasePath) const
Get the dependencies JSON file path for a compilation database.
QString getCacheDirectory(const QString &compilationDatabasePath) const
Get the cache directory for a compilation database.
static AppConfig & instance()
Get the singleton instance.
void loadCompilationDatabase(const QString &compilationDatabasePath, const QString &projectRoot=QString())
Load compilation database and populate translation units.
static void checkpoint(const QString &label)
Log current memory usage with a label.