ACAV f0ba4b7c9529
Abstract Syntax Tree (AST) visualization tool for C, C++, and Objective-C
Loading...
Searching...
No Matches
SourceCodeView.cpp
1/*$!{
2* Aurora Clang AST Viewer (ACAV)
3*
4* Copyright (c) 2026 Min Liu
5* Copyright (c) 2026 Michael David Adams
6*
7* SPDX-License-Identifier: GPL-2.0-or-later
8*
9* This program is free software; you can redistribute it and/or modify
10* it under the terms of the GNU General Public License as published by
11* the Free Software Foundation; either version 2 of the License, or
12* (at your option) any later version.
13*
14* This program is distributed in the hope that it will be useful,
15* but WITHOUT ANY WARRANTY; without even the implied warranty of
16* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17* GNU General Public License for more details.
18*
19* You should have received a copy of the GNU General Public License along
20* with this program; if not, see <https://www.gnu.org/licenses/>.
21}$!*/
22
23#include "ui/SourceCodeView.h"
25#include "ui/LineNumberArea.h"
26#include <QApplication>
27#include <QFile>
28#include <QFont>
29#include <QKeyEvent>
30#include <QMouseEvent>
31#include <QPainter>
32#include <QPalette>
33#include <QTextBlock>
34#include <QTextDocument>
35#include <QTextStream>
36
37namespace acav {
38
39namespace {
40
41class CursorSignalBlocker {
42public:
43 explicit CursorSignalBlocker(bool &flag) : flag_(flag), previous_(flag) {
44 flag_ = true;
45 }
46 ~CursorSignalBlocker() { flag_ = previous_; }
47
48private:
49 bool &flag_;
50 bool previous_;
51};
52
53} // namespace
54
55SourceCodeView::SourceCodeView(QWidget *parent)
56 : QPlainTextEdit(parent), lineNumberArea_(nullptr) {
57 setupEditor();
58 keywordHighlighter_ = new CppSyntaxHighlighter(document());
59
60 // Create line number area
61 lineNumberArea_ = new LineNumberArea(this);
62
63 // Connect signals for line number updates
64 connect(this, &SourceCodeView::blockCountChanged, this,
65 &SourceCodeView::updateLineNumberAreaWidth);
66 connect(this, &SourceCodeView::updateRequest, this,
67 &SourceCodeView::updateLineNumberArea);
68 connect(this, &SourceCodeView::cursorPositionChanged, this,
69 &SourceCodeView::highlightCurrentLine);
70
71 // Initial setup
72 updateLineNumberAreaWidth(0);
73 highlightCurrentLine();
74}
75
76void SourceCodeView::setupEditor() {
77 // Make read-only but allow cursor movement with keyboard
78 setReadOnly(true);
79 setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
80
81 // Use the default UI font
82 QFontMetrics metrics(font());
83 setTabStopDistance(4 * metrics.horizontalAdvance(' '));
84
85 // Enable line wrapping
86 setLineWrapMode(QPlainTextEdit::NoWrap);
87
88 // Enable always-visible scroll bars
89 setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
90 setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
91
92 // Set some basic styling
93 setObjectName("sourceCodeView");
94 // Styled by QPlainTextEdit#sourceCodeView in style.qss
95}
96
97void SourceCodeView::applyFontSize(int pointSize) {
98 QFont font = this->font();
99 font.setPointSize(pointSize);
100 setFont(font);
101
102 QFontMetrics metrics(font);
103 setTabStopDistance(4 * metrics.horizontalAdvance(' '));
104 updateLineNumberAreaWidth(0);
105 lineNumberArea_->update();
106}
107
108bool SourceCodeView::loadFile(const QString &filePath) {
109 QFile file(filePath);
110 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
111 QString errorMsg =
112 QString("Failed to open file: %1").arg(file.errorString());
113 emit fileLoadError(errorMsg);
114 return false;
115 }
116
117 QTextStream in(&file);
118 QString content = in.readAll();
119 file.close();
120
121 // QPlainTextEdit treats a trailing line break as a new empty block, which
122 // shows up as an extra blank line/line number. Trim exactly one trailing
123 // line break so the displayed line count matches common tools/editors.
124 if (content.endsWith(QStringLiteral("\r\n"))) {
125 content.chop(2);
126 } else if (content.endsWith('\n') || content.endsWith('\r')) {
127 content.chop(1);
128 }
129
130 CursorSignalBlocker blockSignals(suppressCursorSignal_);
131 setPlainText(content);
133 searchCursor_ = QTextCursor();
134 lastSearchTerm_.clear();
135 currentFilePath_ = filePath;
136
137 emit fileLoaded(filePath);
138 return true;
139}
140
142 clear();
143 currentFilePath_.clear();
144 currentFileId_ = FileManager::InvalidFileID;
147 searchCursor_ = QTextCursor();
148 lastSearchTerm_.clear();
149}
150
151void SourceCodeView::highlightRange(const SourceRange &range, bool moveCursor) {
152 // Verify range is in current file
153 if (range.begin().fileID() != currentFileId_ ||
154 currentFileId_ == FileManager::InvalidFileID) {
155 return;
156 }
157
158 // Convert SourceRange to QTextCursor
159 QTextDocument *doc = document();
160
161 // Find begin position (line and column are 1-based)
162 QTextBlock beginBlock = doc->findBlockByLineNumber(range.begin().line() - 1);
163 if (!beginBlock.isValid()) {
164 return;
165 }
166 int beginPos = beginBlock.position() + range.begin().column() - 1;
167
168 // Find end position
169 QTextBlock endBlock = doc->findBlockByLineNumber(range.end().line() - 1);
170 if (!endBlock.isValid()) {
171 return;
172 }
173 int endPos = endBlock.position() + range.end().column() - 1;
174
175 // Create cursor and select range
176 QTextCursor cursor(doc);
177 cursor.setPosition(beginPos);
178 cursor.setPosition(endPos, QTextCursor::KeepAnchor);
179
180 // Store navigation highlight
181 navigationHighlight_.cursor = cursor;
182 QPalette pal = palette();
183 navigationHighlight_.format.setBackground(
184 pal.brush(QPalette::Highlight));
185 navigationHighlight_.format.setForeground(
186 pal.brush(QPalette::HighlightedText));
187
188 // Update display
189 updateHighlights();
190
191 if (moveCursor) {
192 // Scroll to beginning of selection
193 CursorSignalBlocker blockSignals(suppressCursorSignal_);
194 QTextCursor scrollCursor = textCursor();
195 scrollCursor.setPosition(beginPos);
196 setTextCursor(scrollCursor);
197 ensureCursorVisible();
198 }
199}
200
201bool SourceCodeView::findNext(const QString &term,
202 QTextDocument::FindFlags flags) {
203 return performFind(term, flags);
204}
205
206bool SourceCodeView::findPrevious(const QString &term,
207 QTextDocument::FindFlags flags) {
208 return performFind(term, flags | QTextDocument::FindBackward);
209}
210
212 searchHighlight_ = QTextEdit::ExtraSelection();
213 searchCursor_ = QTextCursor();
214 updateHighlights();
215}
216
218 navigationHighlight_ = QTextEdit::ExtraSelection();
219 updateHighlights();
220}
221
222void SourceCodeView::updateHighlights() {
223 QList<QTextEdit::ExtraSelection> extraSelections;
224
225 // Add search highlight first so navigation highlight can override color
226 if (!searchHighlight_.cursor.isNull()) {
227 extraSelections.append(searchHighlight_);
228 }
229
230 // Add navigation highlight if present
231 if (!navigationHighlight_.cursor.isNull()) {
232 extraSelections.append(navigationHighlight_);
233 }
234
235 setExtraSelections(extraSelections);
236}
237
238bool SourceCodeView::performFind(const QString &term,
239 QTextDocument::FindFlags flags) {
240 if (term.isEmpty()) {
241 clearSearchHighlight();
242 lastSearchTerm_.clear();
243 return false;
244 }
245
246 QTextDocument *doc = document();
247 if (!doc) {
248 return false;
249 }
250
251 const bool backward = flags.testFlag(QTextDocument::FindBackward);
252 QTextCursor startCursor;
253
254 if (lastSearchTerm_ == term && !searchCursor_.isNull()) {
255 startCursor = searchCursor_;
256 if (backward) {
257 startCursor.setPosition(searchCursor_.selectionStart());
258 } else {
259 startCursor.setPosition(searchCursor_.selectionEnd());
260 }
261 } else {
262 startCursor = textCursor();
263 }
264
265 QTextCursor match = doc->find(term, startCursor, flags);
266
267 if (match.isNull()) {
268 QTextCursor wrapCursor(doc);
269 if (backward) {
270 wrapCursor.movePosition(QTextCursor::End);
271 } else {
272 wrapCursor.movePosition(QTextCursor::Start);
273 }
274 match = doc->find(term, wrapCursor, flags);
275 }
276
277 if (match.isNull()) {
278 clearSearchHighlight();
279 searchCursor_ = QTextCursor();
280 return false;
281 }
282
283 lastSearchTerm_ = term;
284 searchCursor_ = match;
285 CursorSignalBlocker blockSignals(suppressCursorSignal_);
286 setTextCursor(match);
287 setSearchHighlight(match);
288 ensureCursorVisible();
289 return true;
290}
291
292void SourceCodeView::setSearchHighlight(const QTextCursor &cursor) {
293 searchHighlight_ = QTextEdit::ExtraSelection();
294 searchHighlight_.cursor = cursor;
295 searchHighlight_.format.setBackground(QColor(255, 235, 59, 160));
296 updateHighlights();
297}
298
300 int digits = 1;
301 int max = qMax(1, blockCount());
302 while (max >= 10) {
303 max /= 10;
304 ++digits;
305 }
306
307 // Space for digits + padding on both sides
308 int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;
309 return space;
310}
311
313 QPainter painter(lineNumberArea_);
314 painter.fillRect(event->rect(),
315 QColor(240, 240, 240)); // Light gray background
316
317 // Use the same font as the editor
318 painter.setFont(font());
319
320 QTextBlock block = firstVisibleBlock();
321 int blockNumber = block.blockNumber();
322 int top =
323 qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
324 int bottom = top + qRound(blockBoundingRect(block).height());
325
326 while (block.isValid() && top <= event->rect().bottom()) {
327 if (block.isVisible() && bottom >= event->rect().top()) {
328 QString number = QString::number(blockNumber + 1); // 1-based line numbers
329 painter.setPen(QColor(100, 100, 100)); // Dark gray text
330 painter.drawText(0, top, lineNumberArea_->width(), fontMetrics().height(),
331 Qt::AlignRight, number);
332 }
333
334 block = block.next();
335 top = bottom;
336 bottom = top + qRound(blockBoundingRect(block).height());
337 ++blockNumber;
338 }
339}
340
341void SourceCodeView::resizeEvent(QResizeEvent *event) {
342 QPlainTextEdit::resizeEvent(event);
343
344 QRect cr = contentsRect();
345 lineNumberArea_->setGeometry(
346 QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
347}
348
349void SourceCodeView::updateLineNumberAreaWidth(int /* newBlockCount */) {
350 setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
351}
352
353void SourceCodeView::updateLineNumberArea(const QRect &rect, int dy) {
354 if (dy) {
355 lineNumberArea_->scroll(0, dy);
356 } else {
357 lineNumberArea_->update(0, rect.y(), lineNumberArea_->width(),
358 rect.height());
359 }
360
361 if (rect.contains(viewport()->rect())) {
362 updateLineNumberAreaWidth(0);
363 }
364}
365
366void SourceCodeView::highlightCurrentLine() {
367 // Don't highlight current line - we're read-only and it would interfere
368 // with navigation highlighting. Just update the existing highlights.
369 updateHighlights();
370 if (!suppressCursorSignal_) {
371 emitCursorPosition();
372 }
373}
374
375void SourceCodeView::mousePressEvent(QMouseEvent *event) {
376 if (event->button() == Qt::LeftButton) {
379 mouseSelectionActive_ = true;
380 pressMousePos_ = event->pos();
381 QTextCursor cursor = textCursor();
382 pressSelectionStart_ = cursor.selectionStart();
383 pressSelectionEnd_ = cursor.selectionEnd();
384 }
385
386 // Call base implementation after capturing current state
387 QPlainTextEdit::mousePressEvent(event);
388
389 // mouseSelectionActive_ stays true until release.
390}
391
392void SourceCodeView::mouseReleaseEvent(QMouseEvent *event) {
393 // Call base implementation first
394 QPlainTextEdit::mouseReleaseEvent(event);
395
396 if (event->button() != Qt::LeftButton) {
397 return;
398 }
399
400 mouseSelectionActive_ = false;
401
402 QTextCursor cursor = textCursor();
403 bool selectionChanged = cursor.selectionStart() != pressSelectionStart_ ||
404 cursor.selectionEnd() != pressSelectionEnd_;
405 bool movedEnough =
406 (event->pos() - pressMousePos_).manhattanLength() >=
407 QApplication::startDragDistance();
408
409 if (cursor.hasSelection() && (selectionChanged || movedEnough)) {
410 emitSelectionRange();
411 return;
412 }
413
414 emitCursorPosition();
415}
416
417void SourceCodeView::keyPressEvent(QKeyEvent *event) {
418 // Handle Home/End keys for document navigation (not line navigation)
419 // This makes behavior consistent with other panes (AST Tree, File Explorer)
420 if (event->key() == Qt::Key_Home || event->key() == Qt::Key_End) {
421 QTextCursor cursor = textCursor();
422 int oldPos = cursor.position();
423 int oldSelStart = cursor.selectionStart();
424 int oldSelEnd = cursor.selectionEnd();
425
426 QTextCursor::MoveMode moveMode = event->modifiers() & Qt::ShiftModifier
427 ? QTextCursor::KeepAnchor
428 : QTextCursor::MoveAnchor;
429
430 if (event->key() == Qt::Key_Home) {
431 cursor.movePosition(QTextCursor::Start, moveMode);
432 } else {
433 cursor.movePosition(QTextCursor::End, moveMode);
434 }
435
436 setTextCursor(cursor);
437 ensureCursorVisible();
438
439 if (currentFileId_ == FileManager::InvalidFileID) {
440 return;
441 }
442
443 if (cursor.hasSelection() && (cursor.selectionStart() != oldSelStart ||
444 cursor.selectionEnd() != oldSelEnd)) {
445 emitSelectionRange();
446 } else if (cursor.position() != oldPos) {
447 emitCursorPosition();
448 }
449 return;
450 }
451
452 QTextCursor oldCursor = textCursor();
453 int oldPos = oldCursor.position();
454 int oldSelStart = oldCursor.selectionStart();
455 int oldSelEnd = oldCursor.selectionEnd();
456
457 QPlainTextEdit::keyPressEvent(event);
458
459 if (currentFileId_ == FileManager::InvalidFileID) {
460 return;
461 }
462
463 QTextCursor cursor = textCursor();
464 if (cursor.hasSelection() &&
465 (cursor.selectionStart() != oldSelStart ||
466 cursor.selectionEnd() != oldSelEnd)) {
467 emitSelectionRange();
468 return;
469 }
470
471 if (cursor.position() != oldPos) {
472 emitCursorPosition();
473 }
474}
475
476void SourceCodeView::emitCursorPosition() {
477 if (currentFileId_ == FileManager::InvalidFileID || mouseSelectionActive_) {
478 return;
479 }
480
481 QTextCursor cursor = textCursor();
482 if (cursor.hasSelection()) {
483 return;
484 }
485
486 QTextBlock block = cursor.block();
487 unsigned line = block.blockNumber() + 1; // 1-based
488 unsigned column = cursor.positionInBlock() + 1; // 1-based
489
490 emit sourcePositionClicked(currentFileId_, line, column);
491}
492
493void SourceCodeView::emitSelectionRange() {
494 if (currentFileId_ == FileManager::InvalidFileID) {
495 return;
496 }
497
498 QTextCursor cursor = textCursor();
499 if (!cursor.hasSelection()) {
500 return;
501 }
502
503 int startPos = cursor.selectionStart();
504 int endPos = cursor.selectionEnd();
505 if (endPos <= startPos) {
506 return;
507 }
508
509 QTextDocument *doc = document();
510 if (!doc) {
511 return;
512 }
513
514 QTextBlock startBlock = doc->findBlock(startPos);
515 QTextBlock endBlock = doc->findBlock(endPos);
516 if (!startBlock.isValid() || !endBlock.isValid()) {
517 return;
518 }
519
520 unsigned startLine = startBlock.blockNumber() + 1;
521 unsigned startColumn =
522 static_cast<unsigned>(startPos - startBlock.position() + 1);
523 unsigned endLine = endBlock.blockNumber() + 1;
524 unsigned endColumn =
525 static_cast<unsigned>(endPos - endBlock.position() + 1);
526
527 emit sourceRangeSelected(currentFileId_, startLine, startColumn, endLine,
528 endColumn);
529}
530
531} // namespace acav
Lightweight C/C++ syntax highlighter for SourceCodeView.
Source code display widget with line numbers.
static constexpr FileID InvalidFileID
Reserved invalid FileID.
Definition FileManager.h:47
void keyPressEvent(QKeyEvent *event) override
Override to emit navigation on keyboard movement.
bool findPrevious(const QString &term, QTextDocument::FindFlags flags=QTextDocument::FindFlags())
Find previous occurrence of the term and highlight it.
void fileLoadError(const QString &errorMessage)
Emitted when file loading fails.
bool loadFile(const QString &filePath)
Load and display a source file.
void clearSearchHighlight()
Clear any active search highlight.
void resizeEvent(QResizeEvent *event) override
Override to resize line number area with editor.
void mouseReleaseEvent(QMouseEvent *event) override
Override to detect mouse selection end.
void highlightRange(const SourceRange &range, bool moveCursor=true)
Highlight source range with light green background.
int lineNumberAreaWidth() const
Calculate required width for line number area.
void mousePressEvent(QMouseEvent *event) override
Override to detect mouse press for selection tracking.
void fileLoaded(const QString &filePath)
Emitted when a file is successfully loaded.
bool findNext(const QString &term, QTextDocument::FindFlags flags=QTextDocument::FindFlags())
Find next occurrence of the term and highlight it.
void applyFontSize(int pointSize)
Update font size and refresh editor metrics.
void lineNumberAreaPaintEvent(QPaintEvent *event)
Paint line numbers for visible text blocks Called by LineNumberArea::paintEvent().
void clearView()
Clear the view.
void clearHighlight()
Clear current highlight.
Represents a span of source code (begin to end).