25#include "ui/LineNumberArea.h"
26#include <QApplication>
34#include <QTextDocument>
41class CursorSignalBlocker {
43 explicit CursorSignalBlocker(
bool &flag) : flag_(flag), previous_(flag) {
46 ~CursorSignalBlocker() { flag_ = previous_; }
55SourceCodeView::SourceCodeView(QWidget *parent)
56 : QPlainTextEdit(parent), lineNumberArea_(nullptr) {
58 keywordHighlighter_ =
new CppSyntaxHighlighter(document());
61 lineNumberArea_ =
new LineNumberArea(
this);
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);
72 updateLineNumberAreaWidth(0);
73 highlightCurrentLine();
76void SourceCodeView::setupEditor() {
79 setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
82 QFontMetrics metrics(font());
83 setTabStopDistance(4 * metrics.horizontalAdvance(
' '));
86 setLineWrapMode(QPlainTextEdit::NoWrap);
89 setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
90 setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
93 setObjectName(
"sourceCodeView");
98 QFont font = this->font();
99 font.setPointSize(pointSize);
102 QFontMetrics metrics(font);
103 setTabStopDistance(4 * metrics.horizontalAdvance(
' '));
104 updateLineNumberAreaWidth(0);
105 lineNumberArea_->update();
109 QFile file(filePath);
110 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
112 QString(
"Failed to open file: %1").arg(file.errorString());
117 QTextStream in(&file);
118 QString content = in.readAll();
124 if (content.endsWith(QStringLiteral(
"\r\n"))) {
126 }
else if (content.endsWith(
'\n') || content.endsWith(
'\r')) {
130 CursorSignalBlocker blockSignals(suppressCursorSignal_);
131 setPlainText(content);
133 searchCursor_ = QTextCursor();
134 lastSearchTerm_.clear();
135 currentFilePath_ = filePath;
143 currentFilePath_.clear();
147 searchCursor_ = QTextCursor();
148 lastSearchTerm_.clear();
153 if (range.begin().fileID() != currentFileId_ ||
159 QTextDocument *doc = document();
162 QTextBlock beginBlock = doc->findBlockByLineNumber(range.begin().line() - 1);
163 if (!beginBlock.isValid()) {
166 int beginPos = beginBlock.position() + range.begin().column() - 1;
169 QTextBlock endBlock = doc->findBlockByLineNumber(range.end().line() - 1);
170 if (!endBlock.isValid()) {
173 int endPos = endBlock.position() + range.end().column() - 1;
176 QTextCursor cursor(doc);
177 cursor.setPosition(beginPos);
178 cursor.setPosition(endPos, QTextCursor::KeepAnchor);
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));
193 CursorSignalBlocker blockSignals(suppressCursorSignal_);
194 QTextCursor scrollCursor = textCursor();
195 scrollCursor.setPosition(beginPos);
196 setTextCursor(scrollCursor);
197 ensureCursorVisible();
202 QTextDocument::FindFlags flags) {
203 return performFind(term, flags);
207 QTextDocument::FindFlags flags) {
208 return performFind(term, flags | QTextDocument::FindBackward);
212 searchHighlight_ = QTextEdit::ExtraSelection();
213 searchCursor_ = QTextCursor();
218 navigationHighlight_ = QTextEdit::ExtraSelection();
222void SourceCodeView::updateHighlights() {
223 QList<QTextEdit::ExtraSelection> extraSelections;
226 if (!searchHighlight_.cursor.isNull()) {
227 extraSelections.append(searchHighlight_);
231 if (!navigationHighlight_.cursor.isNull()) {
232 extraSelections.append(navigationHighlight_);
235 setExtraSelections(extraSelections);
238bool SourceCodeView::performFind(
const QString &term,
239 QTextDocument::FindFlags flags) {
240 if (term.isEmpty()) {
241 clearSearchHighlight();
242 lastSearchTerm_.clear();
246 QTextDocument *doc = document();
251 const bool backward = flags.testFlag(QTextDocument::FindBackward);
252 QTextCursor startCursor;
254 if (lastSearchTerm_ == term && !searchCursor_.isNull()) {
255 startCursor = searchCursor_;
257 startCursor.setPosition(searchCursor_.selectionStart());
259 startCursor.setPosition(searchCursor_.selectionEnd());
262 startCursor = textCursor();
265 QTextCursor match = doc->find(term, startCursor, flags);
267 if (match.isNull()) {
268 QTextCursor wrapCursor(doc);
270 wrapCursor.movePosition(QTextCursor::End);
272 wrapCursor.movePosition(QTextCursor::Start);
274 match = doc->find(term, wrapCursor, flags);
277 if (match.isNull()) {
278 clearSearchHighlight();
279 searchCursor_ = QTextCursor();
283 lastSearchTerm_ = term;
284 searchCursor_ = match;
285 CursorSignalBlocker blockSignals(suppressCursorSignal_);
286 setTextCursor(match);
287 setSearchHighlight(match);
288 ensureCursorVisible();
292void SourceCodeView::setSearchHighlight(
const QTextCursor &cursor) {
293 searchHighlight_ = QTextEdit::ExtraSelection();
294 searchHighlight_.cursor = cursor;
295 searchHighlight_.format.setBackground(QColor(255, 235, 59, 160));
301 int max = qMax(1, blockCount());
308 int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char(
'9')) * digits;
313 QPainter painter(lineNumberArea_);
314 painter.fillRect(event->rect(),
315 QColor(240, 240, 240));
318 painter.setFont(font());
320 QTextBlock block = firstVisibleBlock();
321 int blockNumber = block.blockNumber();
323 qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
324 int bottom = top + qRound(blockBoundingRect(block).height());
326 while (block.isValid() && top <= event->rect().bottom()) {
327 if (block.isVisible() && bottom >= event->rect().top()) {
328 QString number = QString::number(blockNumber + 1);
329 painter.setPen(QColor(100, 100, 100));
330 painter.drawText(0, top, lineNumberArea_->width(), fontMetrics().height(),
331 Qt::AlignRight, number);
334 block = block.next();
336 bottom = top + qRound(blockBoundingRect(block).height());
342 QPlainTextEdit::resizeEvent(event);
344 QRect cr = contentsRect();
345 lineNumberArea_->setGeometry(
349void SourceCodeView::updateLineNumberAreaWidth(
int ) {
350 setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
353void SourceCodeView::updateLineNumberArea(
const QRect &rect,
int dy) {
355 lineNumberArea_->scroll(0, dy);
357 lineNumberArea_->update(0, rect.y(), lineNumberArea_->width(),
361 if (rect.contains(viewport()->rect())) {
362 updateLineNumberAreaWidth(0);
366void SourceCodeView::highlightCurrentLine() {
370 if (!suppressCursorSignal_) {
371 emitCursorPosition();
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();
387 QPlainTextEdit::mousePressEvent(event);
394 QPlainTextEdit::mouseReleaseEvent(event);
396 if (event->button() != Qt::LeftButton) {
400 mouseSelectionActive_ =
false;
402 QTextCursor cursor = textCursor();
403 bool selectionChanged = cursor.selectionStart() != pressSelectionStart_ ||
404 cursor.selectionEnd() != pressSelectionEnd_;
406 (
event->pos() - pressMousePos_).manhattanLength() >=
407 QApplication::startDragDistance();
409 if (cursor.hasSelection() && (selectionChanged || movedEnough)) {
410 emitSelectionRange();
414 emitCursorPosition();
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();
426 QTextCursor::MoveMode moveMode =
event->modifiers() & Qt::ShiftModifier
427 ? QTextCursor::KeepAnchor
428 : QTextCursor::MoveAnchor;
430 if (event->key() == Qt::Key_Home) {
431 cursor.movePosition(QTextCursor::Start, moveMode);
433 cursor.movePosition(QTextCursor::End, moveMode);
436 setTextCursor(cursor);
437 ensureCursorVisible();
443 if (cursor.hasSelection() && (cursor.selectionStart() != oldSelStart ||
444 cursor.selectionEnd() != oldSelEnd)) {
445 emitSelectionRange();
446 }
else if (cursor.position() != oldPos) {
447 emitCursorPosition();
452 QTextCursor oldCursor = textCursor();
453 int oldPos = oldCursor.position();
454 int oldSelStart = oldCursor.selectionStart();
455 int oldSelEnd = oldCursor.selectionEnd();
457 QPlainTextEdit::keyPressEvent(event);
463 QTextCursor cursor = textCursor();
464 if (cursor.hasSelection() &&
465 (cursor.selectionStart() != oldSelStart ||
466 cursor.selectionEnd() != oldSelEnd)) {
467 emitSelectionRange();
471 if (cursor.position() != oldPos) {
472 emitCursorPosition();
476void SourceCodeView::emitCursorPosition() {
481 QTextCursor cursor = textCursor();
482 if (cursor.hasSelection()) {
486 QTextBlock block = cursor.block();
487 unsigned line = block.blockNumber() + 1;
488 unsigned column = cursor.positionInBlock() + 1;
490 emit sourcePositionClicked(currentFileId_, line, column);
493void SourceCodeView::emitSelectionRange() {
494 if (currentFileId_ == FileManager::InvalidFileID) {
498 QTextCursor cursor = textCursor();
499 if (!cursor.hasSelection()) {
503 int startPos = cursor.selectionStart();
504 int endPos = cursor.selectionEnd();
505 if (endPos <= startPos) {
509 QTextDocument *doc = document();
514 QTextBlock startBlock = doc->findBlock(startPos);
515 QTextBlock endBlock = doc->findBlock(endPos);
516 if (!startBlock.isValid() || !endBlock.isValid()) {
520 unsigned startLine = startBlock.blockNumber() + 1;
521 unsigned startColumn =
522 static_cast<unsigned>(startPos - startBlock.position() + 1);
523 unsigned endLine = endBlock.blockNumber() + 1;
525 static_cast<unsigned>(endPos - endBlock.position() + 1);
527 emit sourceRangeSelected(currentFileId_, startLine, startColumn, endLine,
Lightweight C/C++ syntax highlighter for SourceCodeView.
Source code display widget with line numbers.
static constexpr FileID InvalidFileID
Reserved invalid FileID.
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).