// SPDX-FileCopyrightText: 2021 Ernie Pasveer <epasveer@att.net>
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include "SeerEditorWidgetSource.h"
#include "SeerHighlighterSettings.h"
#include "SeerPlainTextEdit.h"
#include "SeerBreakpointCreateDialog.h"
#include "SeerPrintpointCreateDialog.h"
#include "SeerSourceHighlighter.h"
#include "SeerUtl.h"
#include <QtGui/QColor>
#include <QtGui/QPainter>
#include <QtGui/QTextBlock>
#include <QtGui/QFont>
#include <QtGui/QIcon>
#include <QtGui/QRadialGradient>
#include <QtGui/QHelpEvent>
#include <QtGui/QPainterPath>
#include <QtGui/QGuiApplication>
#include <QtWidgets/QScrollBar>
#include <QtWidgets/QMenu>
#include <QtWidgets/QToolTip>
#include <QtWidgets/QMessageBox>
#include <QtGui/QTextCursor>
#include <QtGui/QPalette>
#include <QtGui/QAction>
#include <QtCore/QList>
#include <QtCore/QString>
#include <QtCore/QTextStream>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QDebug>
#include <QtCore/QCoreApplication>
#include <QtCore/QProcess>

bool SeerEditorWidgetSourceArea::_altHeld = false;
QTimer* SeerEditorWidgetSourceArea::_altHeldTimer = new QTimer();

SeerEditorWidgetSourceArea::SeerEditorWidgetSourceArea(QWidget* parent) : SeerPlainTextEdit(parent) {

    _fileWatcher                = 0;
    _enableLineNumberArea       = false;
    _enableBreakPointArea       = false;
    _enableMiniMapArea          = false;
    _miniMapArea                = 0;
    _miniMapPixmapDirty         = true;
    _miniMapContentHeight       = 0;
    _sourceHighlighter          = 0;
    _sourceHighlighterEnabled   = true;
    _sourceTabSize              = 4;
    _autoSourceReload           = false;
    _selectedExpressionId       = Seer::createID();
    _selectedBreakpointId       = Seer::createID();

    QFont font("monospace");
    font.setStyleHint(QFont::Monospace);

    setEditorFont(font);
    setEditorTabSize(4);
    setExternalEditorCommand("");

    setReadOnly(true);
    setTextInteractionFlags(textInteractionFlags() | Qt::TextSelectableByKeyboard);
    setLineWrapMode(QPlainTextEdit::NoWrap);

    _lineNumberArea = new SeerEditorWidgetSourceLineNumberArea(this);
    _breakPointArea = new SeerEditorWidgetSourceBreakPointArea(this);
    _breakPointArea->setMouseTracking(true);
    _miniMapArea    = new SeerEditorWidgetSourceMiniMapArea(this);

    enableLineNumberArea(true);
    enableBreakPointArea(true);
    enableMiniMapArea(false);

    _altHeldTimer->setInterval(40);  // 40 ms interval = 25Hz update rate
    QObject::connect(_altHeldTimer, &QTimer::timeout, this, [this]() {
        updateCursor(QCursor::pos());
    });

    QObject::connect(this, &SeerEditorWidgetSourceArea::blockCountChanged,                  this, &SeerEditorWidgetSourceArea::updateMarginAreasWidth);
    QObject::connect(this, &SeerEditorWidgetSourceArea::blockCountChanged,                  this, &SeerEditorWidgetSourceArea::invalidateMiniMapCache);
    QObject::connect(this, &SeerEditorWidgetSourceArea::updateRequest,                      this, &SeerEditorWidgetSourceArea::updateLineNumberArea);
    QObject::connect(this, &SeerEditorWidgetSourceArea::updateRequest,                      this, &SeerEditorWidgetSourceArea::updateBreakPointArea);
    QObject::connect(this, &SeerEditorWidgetSourceArea::updateRequest,                      this, &SeerEditorWidgetSourceArea::updateMiniMapArea);
    QObject::connect(this, &SeerEditorWidgetSourceArea::highlighterSettingsChanged,         this, &SeerEditorWidgetSourceArea::handleHighlighterSettingsChanged);

    // Connect cursor position changed signal.
    QObject::connect(this, &QPlainTextEdit::cursorPositionChanged,                          this, &SeerEditorWidgetSourceArea::handleCursorPositionChanged);

    setCurrentLine(0);

    updateMarginAreasWidth(0);

    // Forward the scroll events in the various areas to the text edit.
    SeerPlainTextWheelEventForwarder* lineNumberAreaWheelForwarder = new SeerPlainTextWheelEventForwarder(this);
    SeerPlainTextWheelEventForwarder* breakPointAreaWheelForwarder = new SeerPlainTextWheelEventForwarder(this);
    SeerPlainTextWheelEventForwarder* miniMapAreaWheelForwarder    = new SeerPlainTextWheelEventForwarder(this);

    _lineNumberArea->installEventFilter(lineNumberAreaWheelForwarder);
    _breakPointArea->installEventFilter(breakPointAreaWheelForwarder);
    _miniMapArea->installEventFilter(miniMapAreaWheelForwarder);

    _altHeldTimer->start();

    // Calling close() will clear the text document.
    close();
}

void SeerEditorWidgetSourceArea::enableLineNumberArea (bool flag) {

    _enableLineNumberArea = flag;

    updateMarginAreasWidth(0);
}

bool SeerEditorWidgetSourceArea::lineNumberAreaEnabled () const {

    return _enableLineNumberArea;
}

void SeerEditorWidgetSourceArea::enableBreakPointArea (bool flag) {

    _enableBreakPointArea = flag;

    updateMarginAreasWidth(0);
}

bool SeerEditorWidgetSourceArea::breakPointAreaEnabled () const {

    return _enableBreakPointArea;
}

void SeerEditorWidgetSourceArea::enableMiniMapArea (bool flag) {

    _enableMiniMapArea = flag;

    setVerticalScrollBarPolicy(flag ? Qt::ScrollBarAlwaysOff : Qt::ScrollBarAsNeeded);

    invalidateMiniMapCache();
    updateMarginAreasWidth(0);
}

bool SeerEditorWidgetSourceArea::miniMapAreaEnabled () const {

    return _enableMiniMapArea;
}

void SeerEditorWidgetSourceArea::updateMarginAreasWidth (int newBlockCount) {

    Q_UNUSED(newBlockCount);

    int leftMarginWidth  = lineNumberAreaWidth() + breakPointAreaWidth();
    int rightMarginWidth = miniMapAreaWidth();

    setViewportMargins(leftMarginWidth, 0, rightMarginWidth, 0);
}

int SeerEditorWidgetSourceArea::lineNumberAreaWidth () {

    if (lineNumberAreaEnabled() == false) {
        return 0;
    }

    int digits = 1;
    int max    = qMax(1, blockCount());

    while (max >= 10) {
        max /= 10;
        digits++;
    }

    int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;

    return space;
}

int SeerEditorWidgetSourceArea::breakPointAreaWidth () {

    if (breakPointAreaEnabled() == false) {
        return 0;
    }

    int space = 3 + 20;

    return space;
}

int SeerEditorWidgetSourceArea::miniMapAreaWidth () {

    if (miniMapAreaEnabled() == false) {
        return 0;
    }

    return 120;
}

void SeerEditorWidgetSourceArea::updateLineNumberArea (const QRect& rect, int dy) {

    if (lineNumberAreaEnabled() == false) {
        return;
    }

    if (dy) {
        _lineNumberArea->scroll(0, dy);
    }else{
        _lineNumberArea->update(0, rect.y(), _lineNumberArea->width(), rect.height());
    }

    if (rect.contains(viewport()->rect())) {
        updateMarginAreasWidth(0);
    }
}

void SeerEditorWidgetSourceArea::updateBreakPointArea (const QRect& rect, int dy) {

    if (breakPointAreaEnabled() == false) {
        return;
    }

    if (dy) {
        _breakPointArea->scroll(0, dy);
    }else{
        _breakPointArea->update(0, rect.y(), _breakPointArea->width(), rect.height());
    }

    if (rect.contains(viewport()->rect())) {
        updateMarginAreasWidth(0);
    }
}

void SeerEditorWidgetSourceArea::updateMiniMapArea (const QRect& rect, int dy) {

    Q_UNUSED(rect);
    Q_UNUSED(dy);

    if (miniMapAreaEnabled() == false) {
        return;
    }

    // The mini-map's cached content is a whole-document rendering, not a
    // viewport-following strip, so there's nothing to scroll - just repaint
    // the (cheap) viewport-position overlay on top of the cached pixmap.
    _miniMapArea->update();
}

void SeerEditorWidgetSourceArea::invalidateMiniMapCache () {

    _miniMapPixmapDirty = true;

    if (_miniMapArea) {
        _miniMapArea->update();
    }
}

void SeerEditorWidgetSourceArea::lineNumberAreaPaintEvent (QPaintEvent* event) {

    if (lineNumberAreaEnabled() == false) {
        return;
    }

    QTextCharFormat format = highlighterSettings().get("Margin");

    QPainter painter(_lineNumberArea);
    painter.fillRect(event->rect(), format.background().color());
    painter.setPen(format.foreground().color());

    QFont font = painter.font();
    font.setItalic(format.fontItalic());
    font.setWeight(QFont::Weight(format.fontWeight()));
    painter.setFont(font);

    QTextBlock block       = firstVisibleBlock();
    int        blockNumber = block.blockNumber();
    int        top         = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
    int        bottom      = top + qRound(blockBoundingRect(block).height());

    while (block.isValid() && top <= event->rect().bottom()) {

        if (block.isVisible() && bottom >= event->rect().top()) {
            QString number = QString::number(blockNumber + 1);
            painter.drawText(0, top, _lineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, number);
        }

        block  = block.next();
        top    = bottom;
        bottom = top + qRound(blockBoundingRect(block).height());

        blockNumber++;
    }
}

void SeerEditorWidgetSourceArea::breakPointAreaPaintEvent (QPaintEvent* event) {

    if (breakPointAreaEnabled() == false) {
        return;
    }

    QTextCharFormat format = highlighterSettings().get("Margin");

    QPainter painter(_breakPointArea);
    painter.fillRect(event->rect(), format.background().color());
    painter.setPen(format.foreground().color());

    QFont font = painter.font();
    font.setItalic(format.fontItalic());
    font.setWeight(QFont::Weight(format.fontWeight()));
    painter.setFont(font);

    QTextBlock block       = firstVisibleBlock();
    int        blockNumber = block.blockNumber();
    int        top         = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
    int        bottom      = top + qRound(blockBoundingRect(block).height());

    while (block.isValid() && top <= event->rect().bottom()) {

        if (block.isVisible() && bottom >= event->rect().top()) {

            if (hasBreakpointLine(blockNumber+1)) {
                if (breakpointLineEnabled(blockNumber+1)) {
                    QRect rect(_breakPointArea->width() - 20, top, fontMetrics().height(), fontMetrics().height());

                    QPainterPath path;
                    path.addEllipse(rect);

                    QPointF bias = QPointF(rect.width() * .25 * 1.0, rect.height() * .25 * -1.0);

                    QRadialGradient gradient(rect.center(), rect.width() / 2.0, rect.center() + bias);
                    gradient.setColorAt(0.0, QColor(Qt::white));
                    gradient.setColorAt(0.9, QColor(Qt::red));
                    gradient.setColorAt(1.0, QColor(Qt::transparent));
                    painter.fillPath(path,QBrush(gradient));

                }else{
                    QRect rect(_breakPointArea->width() - 20, top, fontMetrics().height(), fontMetrics().height());

                    QPainterPath path;
                    path.addEllipse(rect);

                    QPointF bias = QPointF(rect.width() * .25 * 1.0, rect.height() * .25 * -1.0);

                    QRadialGradient gradient(rect.center(), rect.width() / 2.0, rect.center() + bias);
                    gradient.setColorAt(0.0, QColor(Qt::white));
                    gradient.setColorAt(0.9, QColor(Qt::darkGray));
                    gradient.setColorAt(1.0, Qt::transparent);
                    painter.fillPath(path,QBrush(gradient));
                }
            }
        }

        block  = block.next();
        top    = bottom;
        bottom = top + qRound(blockBoundingRect(block).height());

        blockNumber++;
    }
}

void SeerEditorWidgetSourceArea::miniMapAreaPaintEvent (QPaintEvent* event) {

    Q_UNUSED(event);

    if (miniMapAreaEnabled() == false) {
        return;
    }

    QTextCharFormat marginFormat = highlighterSettings().get("Margin");

    // Rebuild the cached content pixmap if the document, theme, or size changed.
    // Rendering uses the document's real font (so glyph shaping stays correct)
    // and only shrinks the *rasterized* output via a scaled QPainter - unlike
    // shrinking the live font's point size, which is known to break tab-stop
    // rendering (see setEditorFont()'s comments).
    //
    // Text is drawn as plain strings in a single color, deliberately ignoring
    // the syntax highlighter's per-character formats (QTextLayout::draw()
    // would honor those, but at mini-map scale the anti-aliased blend of many
    // small runs of different keyword/comment/string colors just looks like
    // wrong/muddy colors rather than useful highlighting) - lineNumberAreaPaintEvent()/
    // breakPointAreaPaintEvent() walk lines the same way via blockBoundingRect().
    if (_miniMapPixmapDirty || _miniMapPixmap.size() != _miniMapArea->size()) {

        QSize size = _miniMapArea->size();

        _miniMapPixmap = QPixmap(size);

        // Use the editor's own background/text colors (not the gutter's
        // "Margin" color) so the mini-map matches the editor's color scheme.
        _miniMapPixmap.fill(marginFormat.background().color());

        int   maxChars    = 1;
        qreal docHeightPx = 0;

        for (QTextBlock block = document()->firstBlock(); block.isValid(); block = block.next()) {
            maxChars     = qMax(maxChars, block.text().length());
            docHeightPx += blockBoundingRect(block).height();   // also forces the block's layout
        }

        qreal docWidthPx = maxChars * qreal(fontMetrics().horizontalAdvance(QLatin1Char('9')));

        _miniMapContentHeight = 0;

        if (size.isEmpty() == false && docWidthPx > 0 && docHeightPx > 0) {

            qreal sx = size.width()  / docWidthPx;
            qreal sy = size.height() / docHeightPx;

            // Cap the vertical scale so very short files don't get stretched
            // into oversized, blurry-looking lines.
            qreal maxScaleY = 3.0 / qreal(qMax(1, fontMetrics().height()));
            sy = qMin(sy, maxScaleY);

            // Remember the actual on-screen height the content was rendered
            // at (which may be less than the widget's full height, since sy
            // is capped above) so the viewport overlay and click/drag-to-jump
            // math below use the same scale as the cached pixmap - otherwise
            // the overlay drifts out of sync with the rendered content for
            // any file short enough to hit the cap.
            _miniMapContentHeight = docHeightPx * sy;

            QPainter docPainter(&_miniMapPixmap);
            docPainter.setRenderHint(QPainter::Antialiasing, true);
            docPainter.setRenderHint(QPainter::TextAntialiasing, true);
            docPainter.scale(sx, sy);

            docPainter.setFont(font());
            docPainter.setPen(marginFormat.foreground().color());

            qreal         y      = 0;
            QFontMetricsF fm     (font());
            qreal         ascent = fm.ascent();

            for (QTextBlock block = document()->firstBlock(); block.isValid(); block = block.next()) {

                QString text = block.text();
                text.replace(QLatin1Char('\t'), QLatin1String("    "));

                docPainter.drawText(QPointF(0, y + ascent), text);

                y += blockBoundingRect(block).height();
            }
        }

        _miniMapPixmapDirty = false;
    }

    QPainter painter(_miniMapArea);
    painter.drawPixmap(0, 0, _miniMapPixmap);

    // Overlay: highlight the portion of the document currently visible in the editor.
    // Use the content's actual rendered height (not the widget's full height)
    // so the overlay stays aligned with the cached pixmap even when the
    // vertical scale was capped (see above).
    int   totalLines = qMax(1, document()->blockCount());
    qreal pxPerLine  = _miniMapContentHeight / qreal(totalLines);

    int firstLine    = firstVisibleBlock().blockNumber();
    int visibleLines = qMax(1, int(viewport()->height() / qMax(1, fontMetrics().height())) + 1);

    QRectF overlay(0, firstLine * pxPerLine, _miniMapArea->width(), qMax(4.0, visibleLines * pxPerLine));

    QTextCharFormat currentLineFormat = highlighterSettings().get("Current Line");
    QColor          overlayColor      = currentLineFormat.background().color();
    overlayColor.setAlpha(90);

    painter.fillRect(overlay, overlayColor);
    painter.setPen(currentLineFormat.background().color());
    painter.drawRect(overlay.adjusted(0, 0, -1, -1));
}

void SeerEditorWidgetSourceArea::resizeEvent (QResizeEvent* e) {

    QPlainTextEdit::resizeEvent(e);

    QRect cr = contentsRect();

    if (lineNumberAreaEnabled()) {
        _lineNumberArea->setGeometry (QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
    }

    if (breakPointAreaEnabled()) {
        _breakPointArea->setGeometry (QRect(cr.left() + lineNumberAreaWidth(), cr.top(), breakPointAreaWidth(), cr.height()));
    }

    if (miniMapAreaEnabled()) {
        _miniMapArea->setGeometry (QRect(cr.right() - miniMapAreaWidth() + 1, cr.top(), miniMapAreaWidth(), cr.height()));

        // The widget's height changed, so the cached pixmap's vertical scale is stale.
        invalidateMiniMapCache();
    }
}

void SeerEditorWidgetSourceArea::contextMenuEvent (QContextMenuEvent* event) {

    showContextMenu(event);
}

void SeerEditorWidgetSourceArea::mouseReleaseEvent (QMouseEvent* event) {

    QPlainTextEdit::mouseReleaseEvent(event);

    if (textCursor().selectedText() == "") {
        // Nothing selected, clear current expression.
        clearExpression();
        return;
    }

    _selectedExpressionCursor   = textCursor();
    _selectedExpressionPosition = event->pos();
    _selectedExpressionValue    = "";

    // Look for a keyboard modifier to prepend a '*', '&', or '*&'.
    Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();

    if (modifiers == Qt::ControlModifier) {
        _selectedExpressionName = QString("*") + textCursor().selectedText();

    }else if (modifiers == Qt::ShiftModifier) {
        _selectedExpressionName = QString("&") + textCursor().selectedText();

    }else if (modifiers == (Qt::ShiftModifier | Qt::ControlModifier)) {
        _selectedExpressionName = QString("*&") + textCursor().selectedText();

    }else{
        _selectedExpressionName = textCursor().selectedText();
    }

    emit addVariableLoggerExpression(_selectedExpressionName); // For the variable logger.
}

bool SeerEditorWidgetSourceArea::event(QEvent* event) {

    // Handle the ToolTip event.
    if (event->type() == QEvent::ToolTip) {

        while (1) { // Create a region of the code that does one pass.

            // Convert the event to a Help event.
            QHelpEvent* helpEvent = static_cast<QHelpEvent*>(event);

            // Massage the event location to account for the linenumber and breakpoint widgets.
            QPoint pos = QPoint(helpEvent->pos().x() - _lineNumberArea->width() - _breakPointArea->width(), helpEvent->pos().y());

            // Create a cursor at the position so we can get the text underneath at the cursor.
            QTextCursor cursor = cursorForPosition(pos);
            cursor.select(QTextCursor::WordUnderCursor);

            // If the hover text is empty, do nothing. Reset things. Exit this function.
            QString word = cursor.selectedText();

            if (word.isEmpty() == true) {
                hideExpressionTooltip();
                break;
            }

            // Is our cursor the same as the previous.
            if (cursor == _selectedExpressionCursor) {

                // Same word as before? Display the tooltip value.
                if (word == _selectedExpressionName) {

                    // If the tooltip is already visible, refreshen it with
                    // the same value, possibly at a new postion.
                    if (QToolTip::isVisible()) {
                        _selectedExpressionPosition = helpEvent->globalPos();

                        showExpressionTooltip();

                    // If not already visible, refreshen its value.
                    }else{
                        emit evaluateVariableExpression(_selectedExpressionId, _selectedExpressionName); // For the tooltip.
                    }

                // Otherwise, hide any old one.
                }else{
                    hideExpressionTooltip();
                }

            // Otherwise it's a different spot. Create a new request to get the variable's value.
            }else{

                hideExpressionTooltip();

                _selectedExpressionCursor   = cursor;
                _selectedExpressionPosition = helpEvent->globalPos();
                _selectedExpressionName     = word;
                _selectedExpressionValue    = "";

                emit evaluateVariableExpression(_selectedExpressionId, _selectedExpressionName); // For the tooltip.
            }

            break;
        }

        return true;
    }

    // Pass any others to the base class.
    return QPlainTextEdit::event(event);
}

void SeerEditorWidgetSourceArea::showExpressionTooltip () {

    // qDebug() << "Tooltip:" << _selectedExpressionPosition << _selectedExpressionName << _selectedExpressionValue;

    QToolTip::hideText();
    QToolTip::showText(_selectedExpressionPosition, _selectedExpressionName + ": " + Seer::elideText(_selectedExpressionValue, Qt::ElideRight, 100));
}

void SeerEditorWidgetSourceArea::hideExpressionTooltip () {

    QToolTip::hideText();

    _selectedExpressionCursor   = QTextCursor();
    _selectedExpressionPosition = QPoint();
    _selectedExpressionName     = "";
    _selectedExpressionValue    = "";
}

void SeerEditorWidgetSourceArea::refreshExtraSelections () {

    //
    // Merge all the extra selections into one.
    //
    // The current line(s)
    // The searched text.
    //

    // Create an empty list of selections.
    QList<QTextEdit::ExtraSelection> extraSelections;

    // Append the 'current lines' extra selections.
    extraSelections.append(_currentLinesExtraSelections);

    // Append the 'searched text' extra selections.
    extraSelections.append(_findExtraSelections);

    // Give the editor the list of selections.
    // This will remove the old selections and select the new ones.
    setExtraSelections(extraSelections);
}

bool SeerEditorWidgetSourceArea::isOpen () const {

    if (_fullname != "") {
        return true;
    }

    return false;
}

void SeerEditorWidgetSourceArea::open (const QString& fullname, const QString& file, const QString& alternateDirectory) {

    // Delete old file watcher, if any.
    if (_fileWatcher) {
        delete _fileWatcher; _fileWatcher = 0;
    }

    // Close the previous file, if any.
    if (isOpen()) {
        close();
    }

    // Save the filename.
    _fullname           = fullname;
    _file               = file;
    _alternateDirectory = alternateDirectory;

    setDocumentTitle(_fullname);

    // If the filename or file is null, it's a placeholder
    // widget. Show the Seer icon!
    if (_fullname == "" || _file == "") {
        setStyleSheet("QPlainTextEdit {"
            "background: url(:/seer/resources/icons/hicolor/512x512/seergdb.png) center no-repeat;"
            "border: none;"
            "}");
        return;
    }

    //
    // Open the file.
    //
    // See findFile() to see the search paths and order.
    //
    QString filename = findFile(_file, _fullname, _alternateDirectory, _alternateDirectories);

    if (filename == "") {
        QMessageBox::critical(this, "Can't find source file.",  "Can't find : " + _file + "\nIts location may have changed.");

        emit showAlternateBar(true);

        return;
    }

    QFile inputFile(filename);

    bool f = inputFile.open(QIODevice::ReadOnly);
    if (f == false) {

        QMessageBox::critical(this, "Can't read source file.",  "Can't read : " + filename + "\nThe file is there but can't be opened.");

        emit showAlternateBar(true);

        return;
    }

    // Read the file.
    QTextStream stream(&inputFile);

    QString line = stream.readLine();
    QString text;

    while (!line.isNull()) {

        // Expand tabs
        line = Seer::expandTabs(line, editorTabSize(), false);

        // Build up text for file.
        text += line + "\n";

        // Read next line, if available.
        line = stream.readLine();
    };

    // Put the contents in the editor.
    openText(text, _fullname);

    // Watch the file.
    _fileWatcher = new QFileSystemWatcher(this);
    _fileWatcher->addPath(filename);

    QObject::connect(_fileWatcher, &QFileSystemWatcher::fileChanged,      this, &SeerEditorWidgetSourceArea::handleWatchFileModified);
}

void SeerEditorWidgetSourceArea::openText (const QString& text, const QString& file) {

    // Put the contents in the editor.
    setPlainText(text);

    // Set the text cursor to the first line.
    QTextCursor cursor = textCursor();
    cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor, 1);
    setTextCursor(cursor);

    // Add a syntax highlighter.
    if (_sourceHighlighter) {
        delete _sourceHighlighter; _sourceHighlighter = 0;
    }

    _file = file;
    SeerSourceHighlighter* highlighter = SeerSourceHighlighter::getSourceHighlighter(_file, _sourceHighlighterSettings);
    if (highlighter) {

        _sourceHighlighter = highlighter;

        if (highlighterEnabled()) {
            _sourceHighlighter->setDocument(document());
        }else{
            _sourceHighlighter->setDocument(0);
        }

        _sourceHighlighter->setHighlighterSettings(_sourceHighlighterSettings);
        _sourceHighlighter->rehighlight();
    }

    invalidateMiniMapCache();
}

void SeerEditorWidgetSourceArea::reload () {

    // Open the same file again.
    QString fullname           = _fullname;
    QString file               = _file;
    QString alternateDirectory = _alternateDirectory;

    if (fullname == "") return;
    if (file     == "") return;

    // Save old cursor and scroll position.
    int blockno    = textCursor().blockNumber();
    int posinblock = textCursor().positionInBlock();
    int vscrollpos = verticalScrollBar()->value();

    // Reload file.
    open(fullname, file, alternateDirectory);

    // Restore to old cursor and scroll position.
    QTextCursor c = textCursor();

    c.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor, 1);
    c.movePosition(QTextCursor::Down,  QTextCursor::MoveAnchor, blockno);
    c.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, posinblock);

    setTextCursor(c);

    ensureCursorVisible();

    verticalScrollBar()->setValue(vscrollpos);

    // Refresh any breakpoints or current lines in the stackframes.
    emit refreshBreakpointsStackFrames();
}

void SeerEditorWidgetSourceArea::close () {

    setDocumentTitle("");
    setPlainText("");

    clearExpression();

    // Delete old file watcher, if any.
    if (_fileWatcher) {
        delete _fileWatcher; _fileWatcher = 0;
    }
}

const QString& SeerEditorWidgetSourceArea::fullname () const {
    return _fullname;
}

const QString& SeerEditorWidgetSourceArea::file () const {
    return _file;
}

void SeerEditorWidgetSourceArea::setAlternateDirectory (const QString& alternateDirectory) {

    _alternateDirectory = alternateDirectory;
}

const QString& SeerEditorWidgetSourceArea::alternateDirectory () const {

    return _alternateDirectory;
}

void SeerEditorWidgetSourceArea::setAlternateDirectories (const QStringList& alternateDirectories) {

    _alternateDirectories = alternateDirectories;
}

const QStringList& SeerEditorWidgetSourceArea::alternateDirectories () const {

    return _alternateDirectories;
}

QString SeerEditorWidgetSourceArea::findFile (const QString& file, const QString& fullname, const QString& alternateDirectory, const QStringList& alternateDirectories) {

    //
    // This function returns the filename to use to load the source file.
    //
    // 'file' is the short version of the filename. eg: 'source.cpp'
    // 'fullname' is the long version. eg: '/path/to/locate/source.cpp'
    // 'alternateDirectory' is the alternate directory to use if 'fullname' is not found.
    // 'alternateDirectories' is a list of alternate directories if 'fullname' is not found
    // and if 'file' is not in 'alternateDirectory'.
    //
    // 'alternateDirectory' can be blank ("") and usually is. If it isn't blank, then it
    // takes presedence over 'fullname' and 'alternateDirectories'.
    //
    // 'alternateDirectories' is a list of directory locations. It defines the search order.
    // Once a location has the 'file', the search stops and that directory is used.
    //
    // If the 'file' can't be found in any of the locations, a "" is returned.
    //
    // One note about 'fullname'. This is the path that gdb knows of. Gdb extracts the path
    // from the debug information in the executable when it was compiled and linked.
    //

    // Use 'alternateDirectory', if provided.
    if (alternateDirectory != "") {

        QString filename = alternateDirectory + "/" + file;

        if (QFileInfo::exists(filename) == false) {
            return "";
        }

        return filename;
    }

    // Handle 'fullname'.
    if (QFileInfo::exists(fullname) == true) {
        return fullname;
    }

    // Handle 'alternateDirectories'.
    QStringListIterator iter(alternateDirectories);

    while (iter.hasNext()) {

        QString filename = iter.next() + "/" + file;

        if (QFileInfo::exists(filename) == true) {
            return filename;
        }
    }

    // Not found anywhere.
    return "";
}

void SeerEditorWidgetSourceArea::setCurrentLine (int lineno) {

    // Clear current line selections.
    _currentLinesExtraSelections.clear();

    // Highlight if a valid line number is selected.
    if (lineno >= 1) {

        QTextBlock  block  = document()->findBlockByLineNumber(lineno-1);
        QTextCursor cursor = textCursor();

        cursor.setPosition(block.position());
        setTextCursor(cursor);

        _currentLinesExtraSelections.clear();

        QTextCharFormat lineFormat = highlighterSettings().get("Current Line");

        QTextEdit::ExtraSelection selection;
        selection.format.setForeground(lineFormat.foreground());
        selection.format.setBackground(lineFormat.background());
        selection.format.setProperty(QTextFormat::FullWidthSelection, true);
        selection.cursor = textCursor();
        selection.cursor.clearSelection();

        _currentLinesExtraSelections.append(selection);
    }

    // Refresh all the extra selections.
    refreshExtraSelections();
}

void SeerEditorWidgetSourceArea::setCurrentColumn (int colno) {
    QTextCursor cursor = textCursor();

    int lineStartPos = cursor.block().position();
    int newPos       = lineStartPos + (colno - 1);

    cursor.setPosition(newPos);
    setTextCursor(cursor);
}

int SeerEditorWidgetSourceArea::currentLine () const {
    QTextCursor cursor = textCursor();
    return cursor.blockNumber() + 1;;
}

int SeerEditorWidgetSourceArea::currentColumn () const {
    QTextCursor cursor = textCursor();
    return cursor.positionInBlock() + 1;
}

int SeerEditorWidgetSourceArea::firstDisplayLine () const {
    QTextBlock block = firstVisibleBlock();
    return block.blockNumber() + 1;
}

void SeerEditorWidgetSourceArea::scrollToLine (int lineno) {

    // Scroll to the first line if we went before it.
    if (lineno < 1) {
        lineno = 1;
    }

    // Scroll to the last line if we went past it.
    if (lineno > document()->blockCount()) {
        lineno = document()->blockCount();
    }

    QTextBlock  block  = document()->findBlockByLineNumber(lineno-1);
    QTextCursor cursor = textCursor();

    cursor.setPosition(block.position());
    setTextCursor(cursor);

    centerCursor();
}

void SeerEditorWidgetSourceArea::jumpToMiniMapY (int y) {

    if (miniMapAreaEnabled() == false) {
        return;
    }

    int totalLines = qMax(1, document()->blockCount());
    qreal pxPerLine = _miniMapContentHeight / qreal(totalLines);

    if (pxPerLine <= 0) {
        return;
    }

    int lineno = int(y / pxPerLine) + 1;

    scrollToLine(lineno);
}

void SeerEditorWidgetSourceArea::clearCurrentLines () {

    _currentLinesExtraSelections.clear();

    refreshExtraSelections();
}

void SeerEditorWidgetSourceArea::addCurrentLine (int lineno, int level) {

    // Any line will be highlighted with a yellow line.
    // The 'yellow' color is for the current line of the most recent stack frame.
    // The 'grey' color is for older stack frames.
    QTextCharFormat lineFormat;
    if (level == 0) {
        lineFormat = highlighterSettings().get("Current Line");
    }else{
        lineFormat = highlighterSettings().get("Calling Line");
    }

    // Create a selection at the cursor.
    QTextBlock  block  = document()->findBlockByLineNumber(lineno-1);
    QTextCursor cursor = textCursor();

    cursor.setPosition(block.position());

    QTextEdit::ExtraSelection selection;
    selection.format.setForeground(lineFormat.foreground());
    selection.format.setBackground(lineFormat.background());
    selection.format.setProperty(QTextFormat::FullWidthSelection, true);
    selection.cursor = cursor;
    selection.cursor.clearSelection();

    // Add it to the extra selection list.
    _currentLinesExtraSelections.append(selection);

    // Refresh all the extra selections.
    refreshExtraSelections();
}

int SeerEditorWidgetSourceArea::findText (const QString& text, QTextDocument::FindFlags flags) {

    _findExtraSelections.clear();

    if (document()) {

        QTextCharFormat matchFormat = highlighterSettings().get("Match");

        // Build a list of highlights for all matches.
        QTextCursor cursor(document());
        cursor = document()->find(text, cursor, flags);

        while (cursor.isNull() == false) {

            QTextEdit::ExtraSelection extra;
            extra.format = matchFormat;
            extra.cursor = cursor;

            _findExtraSelections.append(extra);

            cursor = document()->find(text, cursor, flags);
        }

        // Move to the next match after out current position.
        find(text, flags);
    }

    refreshExtraSelections();

    return _findExtraSelections.size();
}

void SeerEditorWidgetSourceArea::clearFindText () {

    _findExtraSelections.clear();

    refreshExtraSelections();
}

void SeerEditorWidgetSourceArea::clearBreakpoints () {

    _breakpointsLineNumbers.clear();
    _breakpointsNumbers.clear();
    _breakpointsEnableds.clear();

    repaint();
}

void SeerEditorWidgetSourceArea::addBreakpoint (int number, int lineno, bool enabled) {

    _breakpointsNumbers.push_back(number);
    _breakpointsLineNumbers.push_back(lineno);
    _breakpointsEnableds.push_back(enabled);

    repaint();
}

bool SeerEditorWidgetSourceArea::hasBreakpointNumber (int number) const {
    return _breakpointsNumbers.contains(number);
}

bool SeerEditorWidgetSourceArea::hasBreakpointLine (int lineno) const {
    return _breakpointsLineNumbers.contains(lineno);
}

const QVector<int>& SeerEditorWidgetSourceArea::breakpointNumbers () const {
    return _breakpointsNumbers;
}

const QVector<int>& SeerEditorWidgetSourceArea::breakpointLines () const {
    return _breakpointsLineNumbers;
}

const QVector<bool>& SeerEditorWidgetSourceArea::breakpointEnableds () const {
    return _breakpointsEnableds;
}

int SeerEditorWidgetSourceArea::breakpointLineToNumber (int lineno) const {

    // Map lineno to breakpoint number.
    int i = _breakpointsLineNumbers.indexOf(lineno);

    if (i < 0) {
        return 0;
    }

    return _breakpointsNumbers[i];
}

bool SeerEditorWidgetSourceArea::breakpointLineEnabled (int lineno) const {

    // Look for the lineno and get its index.
    int i = _breakpointsLineNumbers.indexOf(lineno);

    // Not found, return false.
    if (i < 0) {
        return false;
    }

    // Otherwise, return the proper status.
    return _breakpointsEnableds[i];
}

void SeerEditorWidgetSourceArea::breakpointToggle () {

    // Get current lineno.
    int lineno = textCursor().blockNumber() + 1;

    // If there is a breakpoint on the line, toggle it.
    if (hasBreakpointLine(lineno)) {

        // Toggle the breakpoint.
        // Enable if disabled. Disable if enabled.
        if (breakpointLineEnabled(lineno) == false) {
            // Emit the enable breakpoint signal.
            emit enableBreakpoints(QString("%1").arg(breakpointLineToNumber(lineno)));
        }else{
            // Emit the disable breakpoint signal.
            emit deleteBreakpoints(QString("%1").arg(breakpointLineToNumber(lineno)));
        }

    // Otherwise, do a quick create of a new breakpoint.
    }else{
        emit insertBreakpoint(QString("-f --source \"%1\" --line %2").arg(fullname()).arg(lineno));
    }
}

void SeerEditorWidgetSourceArea::runToSelectedLine () {

    // Emit the runToLine signal.
    emit runToLine(fullname(), currentLine());

}

void SeerEditorWidgetSourceArea::showContextMenu (QMouseEvent* event) {

#if QT_VERSION >= 0x060000
    showContextMenu(event->pos(), event->globalPosition());
#else
    showContextMenu(event->pos(), event->globalPos());
#endif
}

void SeerEditorWidgetSourceArea::showContextMenu (QContextMenuEvent* event) {

    showContextMenu(event->pos(), event->globalPos());
}

void SeerEditorWidgetSourceArea::showContextMenu (const QPoint& pos, const QPointF& globalPos) {

    // Get the line number for the cursor position.
    QTextCursor cursor = cursorForPosition(pos);

    int lineno = cursor.blockNumber()+1;

    // Create the menu actions.
    QAction* createBreakpointAction;
    QAction* createPrintpointAction;
    QAction* deleteAction;
    QAction* enableAction;
    QAction* disableAction;
    QAction* runToLineAction;
    QAction* openExternalEditor;
    QAction* addVariableLoggerExpressionAction;
    QAction* addVariableLoggerAsteriskExpressionAction;
    QAction* addVariableLoggerAmpersandExpressionAction;
    QAction* addVariableLoggerAsteriskAmpersandExpressionAction;
    QAction* addVariableLoggerObjcExpressionAction;
    QAction* addVariableTrackerExpressionAction;
    QAction* addVariableTrackerAsteriskExpressionAction;
    QAction* addVariableTrackerAmpersandExpressionAction;
    QAction* addVariableTrackerAsteriskAmpersandExpressionAction;
    QAction* addVariableTrackerObjcExpressionAction;
    QAction* addMemoryVisualizerAction;
    QAction* addMemoryAsteriskVisualizerAction;
    QAction* addMemoryAmpersandVisualizerAction;
    QAction* addArrayVisualizerAction;
    QAction* addArrayAsteriskVisualizerAction;
    QAction* addArrayAmpersandVisualizerAction;
    QAction* addMatrixVisualizerAction;
    QAction* addMatrixAsteriskVisualizerAction;
    QAction* addMatrixAmpersandVisualizerAction;
    QAction* addStructVisualizerAction;
    QAction* addStructAsteriskVisualizerAction;
    QAction* addStructAmpersandVisualizerAction;

    // Enable/disable them depending if the breakpoint already exists.
    if (hasBreakpointLine(lineno) == true) {

        int breakno = breakpointLineToNumber(lineno);

        runToLineAction           = new QAction(QIcon(":/seer/resources/RelaxLightIcons/debug-execute-from-cursor.svg"), QString("Run to line %1").arg(lineno), this);
        createBreakpointAction    = new QAction(QIcon(":/seer/resources/RelaxLightIcons/document-new.svg"),              QString("Create breakpoint on line %1").arg(lineno), this);
        createPrintpointAction    = new QAction(QIcon(":/seer/resources/RelaxLightIcons/document-new.svg"),              QString("Create printpoint on line %1").arg(lineno), this);
        deleteAction              = new QAction(QIcon(":/seer/resources/RelaxLightIcons/edit-delete.svg"),               QString("Delete breakpoint %1 on line %2").arg(breakno).arg(lineno), this);
        enableAction              = new QAction(QIcon(":/seer/resources/RelaxLightIcons/list-add.svg"),                  QString("Enable breakpoint %1 on line %2").arg(breakno).arg(lineno), this);
        disableAction             = new QAction(QIcon(":/seer/resources/RelaxLightIcons/list-remove.svg"),               QString("Disable breakpoint %1 on line %2").arg(breakno).arg(lineno), this);
        openExternalEditor        = new QAction(QIcon(":/seer/resources/RelaxLightIcons/document-new.svg"),              QString("Open external editor on line %1").arg(lineno), this);

        runToLineAction->setEnabled(true);
        createBreakpointAction->setEnabled(false);
        createPrintpointAction->setEnabled(false);
        deleteAction->setEnabled(true);
        enableAction->setEnabled(true);
        disableAction->setEnabled(true);
        openExternalEditor->setEnabled(true);

    }else{
        runToLineAction           = new QAction(QIcon(":/seer/resources/RelaxLightIcons/debug-execute-from-cursor.svg"), QString("Run to line %1").arg(lineno), this);
        createBreakpointAction    = new QAction(QIcon(":/seer/resources/RelaxLightIcons/document-new.svg"),              QString("Create breakpoint on line %1").arg(lineno), this);
        createPrintpointAction    = new QAction(QIcon(":/seer/resources/RelaxLightIcons/document-new.svg"),              QString("Create printpoint on line %1").arg(lineno), this);
        deleteAction              = new QAction(QIcon(":/seer/resources/RelaxLightIcons/edit-delete.svg"),               QString("Delete breakpoint on line %1").arg(lineno), this);
        enableAction              = new QAction(QIcon(":/seer/resources/RelaxLightIcons/list-add.svg"),                  QString("Enable breakpoint on line %1").arg(lineno), this);
        disableAction             = new QAction(QIcon(":/seer/resources/RelaxLightIcons/list-remove.svg"),               QString("Disable breakpoint on line %1").arg(lineno), this);
        openExternalEditor        = new QAction(QIcon(":/seer/resources/RelaxLightIcons/document-new.svg"),              QString("Open file in external editor"), this);

        runToLineAction->setEnabled(true);
        createBreakpointAction->setEnabled(true);
        createPrintpointAction->setEnabled(true);
        deleteAction->setEnabled(false);
        enableAction->setEnabled(false);
        disableAction->setEnabled(false);
        openExternalEditor->setEnabled(true);
    }

    addVariableLoggerExpressionAction                   = new QAction(QString("%1").arg(textCursor().selectedText()));
    addVariableLoggerAsteriskExpressionAction           = new QAction(QString("*%1").arg(textCursor().selectedText()));
    addVariableLoggerAmpersandExpressionAction          = new QAction(QString("&&%1").arg(textCursor().selectedText()));
    addVariableLoggerAsteriskAmpersandExpressionAction  = new QAction(QString("*&&%1").arg(textCursor().selectedText()));
    addVariableLoggerObjcExpressionAction               = new QAction(QString("(objc)%1").arg(textCursor().selectedText()));
    addVariableTrackerExpressionAction                  = new QAction(QString("%1").arg(textCursor().selectedText()));
    addVariableTrackerAsteriskExpressionAction          = new QAction(QString("*%1").arg(textCursor().selectedText()));
    addVariableTrackerAmpersandExpressionAction         = new QAction(QString("&&%1").arg(textCursor().selectedText()));
    addVariableTrackerAsteriskAmpersandExpressionAction = new QAction(QString("*&&%1").arg(textCursor().selectedText()));
    addVariableTrackerObjcExpressionAction              = new QAction(QString("(objc)%1").arg(textCursor().selectedText()));
    addMemoryVisualizerAction                           = new QAction(QString("%1").arg(textCursor().selectedText()));
    addMemoryAsteriskVisualizerAction                   = new QAction(QString("*%1").arg(textCursor().selectedText()));
    addMemoryAmpersandVisualizerAction                  = new QAction(QString("&&%1").arg(textCursor().selectedText()));
    addArrayVisualizerAction                            = new QAction(QString("%1").arg(textCursor().selectedText()));
    addArrayAsteriskVisualizerAction                    = new QAction(QString("*%1").arg(textCursor().selectedText()));
    addArrayAmpersandVisualizerAction                   = new QAction(QString("&&%1").arg(textCursor().selectedText()));
    addMatrixVisualizerAction                           = new QAction(QString("%1").arg(textCursor().selectedText()));
    addMatrixAsteriskVisualizerAction                   = new QAction(QString("*%1").arg(textCursor().selectedText()));
    addMatrixAmpersandVisualizerAction                  = new QAction(QString("&&%1").arg(textCursor().selectedText()));
    addStructVisualizerAction                           = new QAction(QString("%1").arg(textCursor().selectedText()));
    addStructAsteriskVisualizerAction                   = new QAction(QString("*%1").arg(textCursor().selectedText()));
    addStructAmpersandVisualizerAction                  = new QAction(QString("&&%1").arg(textCursor().selectedText()));

    QMenu menu("Breakpoints", this);
    menu.setTitle("Breakpoints");
    menu.addAction(runToLineAction);
    menu.addAction(createBreakpointAction);
    menu.addAction(createPrintpointAction);
    menu.addAction(deleteAction);
    menu.addAction(enableAction);
    menu.addAction(disableAction);
    menu.addAction(openExternalEditor);

    QMenu loggerMenu("Add variable to Logger");
    loggerMenu.addAction(addVariableLoggerExpressionAction);
    loggerMenu.addAction(addVariableLoggerAsteriskExpressionAction);
    loggerMenu.addAction(addVariableLoggerAmpersandExpressionAction);
    loggerMenu.addAction(addVariableLoggerAsteriskAmpersandExpressionAction);
    loggerMenu.addAction(addVariableLoggerObjcExpressionAction);
    menu.addMenu(&loggerMenu);

    QMenu trackerMenu("Add variable to Tracker");
    trackerMenu.addAction(addVariableTrackerExpressionAction);
    trackerMenu.addAction(addVariableTrackerAsteriskExpressionAction);
    trackerMenu.addAction(addVariableTrackerAmpersandExpressionAction);
    trackerMenu.addAction(addVariableTrackerAsteriskAmpersandExpressionAction);
    trackerMenu.addAction(addVariableTrackerObjcExpressionAction);
    menu.addMenu(&trackerMenu);

    QMenu memoryVisualizerMenu("Add variable to a Memory Visualizer");
    memoryVisualizerMenu.addAction(addMemoryVisualizerAction);
    memoryVisualizerMenu.addAction(addMemoryAsteriskVisualizerAction);
    memoryVisualizerMenu.addAction(addMemoryAmpersandVisualizerAction);
    menu.addMenu(&memoryVisualizerMenu);

    QMenu arrayVisualizerMenu("Add variable to an Array Visualizer");
    arrayVisualizerMenu.addAction(addArrayVisualizerAction);
    arrayVisualizerMenu.addAction(addArrayAsteriskVisualizerAction);
    arrayVisualizerMenu.addAction(addArrayAmpersandVisualizerAction);
    menu.addMenu(&arrayVisualizerMenu);

    QMenu matrixVisualizerMenu("Add variable to a Matrix Visualizer");
    matrixVisualizerMenu.addAction(addMatrixVisualizerAction);
    matrixVisualizerMenu.addAction(addMatrixAsteriskVisualizerAction);
    matrixVisualizerMenu.addAction(addMatrixAmpersandVisualizerAction);
    menu.addMenu(&matrixVisualizerMenu);

    QMenu structVisualizerMenu("Add variable to a Struct Visualizer");
    structVisualizerMenu.addAction(addStructVisualizerAction);
    structVisualizerMenu.addAction(addStructAsteriskVisualizerAction);
    structVisualizerMenu.addAction(addStructAmpersandVisualizerAction);
    menu.addMenu(&structVisualizerMenu);

    // Enable/disable items based on something being selected or not.
    if (textCursor().selectedText() == "") {
        addVariableLoggerExpressionAction->setEnabled(false);
        addVariableLoggerAsteriskExpressionAction->setEnabled(false);
        addVariableLoggerAmpersandExpressionAction->setEnabled(false);
        addVariableLoggerAsteriskAmpersandExpressionAction->setEnabled(false);
        addVariableLoggerObjcExpressionAction->setEnabled(false);
        addVariableTrackerExpressionAction->setEnabled(false);
        addVariableTrackerAsteriskExpressionAction->setEnabled(false);
        addVariableTrackerAmpersandExpressionAction->setEnabled(false);
        addVariableTrackerAsteriskAmpersandExpressionAction->setEnabled(false);
        addMemoryVisualizerAction->setEnabled(false);
        addMemoryAsteriskVisualizerAction->setEnabled(false);
        addMemoryAmpersandVisualizerAction->setEnabled(false);
        addArrayVisualizerAction->setEnabled(false);
        addArrayAsteriskVisualizerAction->setEnabled(false);
        addArrayAmpersandVisualizerAction->setEnabled(false);
        addMatrixVisualizerAction->setEnabled(false);
        addMatrixAsteriskVisualizerAction->setEnabled(false);
        addMatrixAmpersandVisualizerAction->setEnabled(false);
        addStructVisualizerAction->setEnabled(false);
        addStructAsteriskVisualizerAction->setEnabled(false);
        addStructAmpersandVisualizerAction->setEnabled(false);
    }else{
        addVariableLoggerExpressionAction->setEnabled(true);
        addVariableLoggerAsteriskExpressionAction->setEnabled(true);
        addVariableLoggerAmpersandExpressionAction->setEnabled(true);
        addVariableLoggerAsteriskAmpersandExpressionAction->setEnabled(true);
        addVariableLoggerObjcExpressionAction->setEnabled(true);
        addVariableTrackerExpressionAction->setEnabled(true);
        addVariableTrackerAsteriskExpressionAction->setEnabled(true);
        addVariableTrackerAmpersandExpressionAction->setEnabled(true);
        addVariableTrackerAsteriskAmpersandExpressionAction->setEnabled(true);
        addMemoryVisualizerAction->setEnabled(true);
        addMemoryAsteriskVisualizerAction->setEnabled(true);
        addMemoryAmpersandVisualizerAction->setEnabled(true);
        addArrayVisualizerAction->setEnabled(true);
        addArrayAsteriskVisualizerAction->setEnabled(true);
        addArrayAmpersandVisualizerAction->setEnabled(true);
        addMatrixVisualizerAction->setEnabled(true);
        addMatrixAsteriskVisualizerAction->setEnabled(true);
        addMatrixAmpersandVisualizerAction->setEnabled(true);
        addStructVisualizerAction->setEnabled(true);
        addStructAsteriskVisualizerAction->setEnabled(true);
        addStructAmpersandVisualizerAction->setEnabled(true);
    }

    // Launch the menu. Get the response.
    QAction* action = menu.exec(globalPos.toPoint());

    // Do nothing.
    if (action == 0) {
        return;
    }

    // Handle running to a line number.
    if (action == runToLineAction) {

        // Emit the runToLine signal.
        emit runToLine(fullname(), lineno);

        return;
    }

    // Handle creating a new breakpoint.
    if (action == createBreakpointAction) {

        SeerBreakpointCreateDialog dlg(this);
        dlg.setFilename(fullname());
        dlg.setLineNumber(QString("%1").arg(lineno));

        int ret = dlg.exec();

        if (ret == 0) {
            return;
        }

        // Emit the create breakpoint signal.
        emit insertBreakpoint(dlg.breakpointText());

        return;
    }

    // Handle creating a new printpoint.
    if (action == createPrintpointAction) {

        SeerPrintpointCreateDialog dlg(this);
        dlg.setFilename(fullname());
        dlg.setLineNumber(QString("%1").arg(lineno));

        int ret = dlg.exec();

        if (ret == 0) {
            return;
        }

        // Build a printpoint specification.
        QString type       = dlg.dprintfType();
        QString function   = dlg.dprintfFunction();
        QString channel    = dlg.dprintfChannel();
        QString parameters = dlg.printpointParameters();

        // If nothing, just return.
        if (parameters == "" || type == "") {
            return;
        }

        // Emit the create breakpoint signal.
        emit insertPrintpoint(type, function, channel, parameters);

        return;
    }

    // Handle deleting a breakpoint.
    if (action == deleteAction) {

        // Emit the delete breakpoint signal.
        emit deleteBreakpoints(QString("%1").arg(breakpointLineToNumber(lineno)));

        return;
    }

    // Handle enabling a breakpoint.
    if (action == enableAction) {

        // Emit the enable breakpoint signal.
        emit enableBreakpoints(QString("%1").arg(breakpointLineToNumber(lineno)));

        return;
    }

    // Handle disabling a breakpoint.
    if (action == disableAction) {

        // Emit the disable breakpoint signal.
        emit disableBreakpoints(QString("%1").arg(breakpointLineToNumber(lineno)));

        return;
    }

    // Handle open code editor at a line number.
    if (action == openExternalEditor) {

        // External editor examples:
        //  $ export SEERGDB_CustomCodeEditor='geany "%{file}":%{line}'
        //  $ export SEERGDB_CustomCodeEditor='kate --line %{line} "%{file}"'
        //  $ export SEERGDB_CustomCodeEditor='gedit "%{file}" +%{line}'
        //  $ export SEERGDB_CustomCodeEditor='konsole -e vim "%{file}" +%{line}'
        //  QString codeEditorCmd = []() { const char* pc = std::getenv("SEERGDB_CustomCodeEditor"); if(pc) return pc; return ""; } ();
        QString codeEditorCmd = externalEditorCommand();

        if (codeEditorCmd == "") {
            QMessageBox::critical(this, "Error!",  "External editor not set.\n\nSee the Editor Config page to set the editor to use.");
            return;
        }

        // Mark cursor as busy.
        QApplication::setOverrideCursor(Qt::BusyCursor);

        codeEditorCmd.replace("%{file}", fullname());
        codeEditorCmd.replace("%{line}", QString::number(lineno));

        QProcess* process = new QProcess(this);
        process->startCommand(codeEditorCmd);

        bool f = process->waitForStarted(5000);

        // Set the cursor back.
        QApplication::restoreOverrideCursor();

        if (f == false) {
            QMessageBox::critical(this, "Error!",  "Launching external editor failed.\n\nCommand: '" + codeEditorCmd + "'");
            qDebug().nospace() << "Launching external editor failed. Command: '" << codeEditorCmd << "'";
            qDebug().nospace() << "Error: '" << process->error() << "'";
        }

        return;
    }

    // Handle adding a variable to log.
    if (action == addVariableLoggerExpressionAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addVariableLoggerExpression(textCursor().selectedText());
        }

        return;
    }

    // Handle adding a variable to log.
    if (action == addVariableLoggerAsteriskExpressionAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addVariableLoggerExpression(QString("*") + textCursor().selectedText());
        }

        return;
    }

    // Handle adding a variable to log.
    if (action == addVariableLoggerAmpersandExpressionAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addVariableLoggerExpression(QString("&") + textCursor().selectedText());
        }

        return;
    }

    // Handle adding a variable to log.
    if (action == addVariableLoggerAsteriskAmpersandExpressionAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addVariableLoggerExpression(QString("*&") + textCursor().selectedText());
        }

        return;
    }

    // Handle adding a variable to log.
    if (action == addVariableLoggerObjcExpressionAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addVariableLoggerExpression(QString("(objc)") + textCursor().selectedText());
        }

        return;
    }

    // Handle adding a variable to track.
    if (action == addVariableTrackerExpressionAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addVariableTrackerExpression(textCursor().selectedText());
            emit refreshVariableTrackerValues();
        }

        return;
    }

    // Handle adding a variable to track.
    if (action == addVariableTrackerAsteriskExpressionAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addVariableTrackerExpression(QString("*") + textCursor().selectedText());
            emit refreshVariableTrackerValues();
        }

        return;
    }

    // Handle adding a variable to track.
    if (action == addVariableTrackerAmpersandExpressionAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addVariableTrackerExpression(QString("&") + textCursor().selectedText());
            emit refreshVariableTrackerValues();
        }

        return;
    }

    // Handle adding a variable to track.
    if (action == addVariableTrackerAsteriskAmpersandExpressionAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addVariableTrackerExpression(QString("*&") + textCursor().selectedText());
            emit refreshVariableTrackerValues();
        }

        return;
    }

    // Handle adding a variable to track.
    if (action == addVariableTrackerObjcExpressionAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addVariableTrackerExpression(QString("(objc)") + textCursor().selectedText());
            emit refreshVariableTrackerValues();
        }

        return;
    }

    // Handle adding memory to visualize.
    if (action == addMemoryVisualizerAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addMemoryVisualizer(textCursor().selectedText());
        }

        return;
    }

    // Handle adding memory to visualize.
    if (action == addMemoryAsteriskVisualizerAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addMemoryVisualizer(QString("*") + textCursor().selectedText());
        }

        return;
    }

    // Handle adding memory to visualize.
    if (action == addMemoryAmpersandVisualizerAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addMemoryVisualizer(QString("&") + textCursor().selectedText());
        }

        return;
    }

    // Handle adding array to visualize.
    if (action == addArrayVisualizerAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addArrayVisualizer(textCursor().selectedText());
        }

        return;
    }

    // Handle adding array to visualize.
    if (action == addArrayAsteriskVisualizerAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addArrayVisualizer(QString("*") + textCursor().selectedText());
        }

        return;
    }

    // Handle adding array to visualize.
    if (action == addArrayAmpersandVisualizerAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addArrayVisualizer(QString("&") + textCursor().selectedText());
        }

        return;
    }

    // Handle adding array to visualize.
    if (action == addMatrixVisualizerAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addMatrixVisualizer(textCursor().selectedText());
        }

        return;
    }

    // Handle adding array to visualize.
    if (action == addMatrixAsteriskVisualizerAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addMatrixVisualizer(QString("*") + textCursor().selectedText());
        }

        return;
    }

    // Handle adding array to visualize.
    if (action == addMatrixAmpersandVisualizerAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addMatrixVisualizer(QString("&") + textCursor().selectedText());
        }

        return;
    }

    // Handle adding struct to visualize.
    if (action == addStructVisualizerAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addStructVisualizer(textCursor().selectedText());
        }

        return;
    }

    // Handle adding struct to visualize.
    if (action == addStructAsteriskVisualizerAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addStructVisualizer(QString("*") + textCursor().selectedText());
        }

        return;
    }

    // Handle adding struct to visualize.
    if (action == addStructAmpersandVisualizerAction) {

        // Emit the signals.
        if (textCursor().selectedText() != "") {
            emit addStructVisualizer(QString("&") + textCursor().selectedText());
        }

        return;
    }
}

void SeerEditorWidgetSourceArea::setQuickBreakpoint (QMouseEvent* event) {

    // Get the line number for the cursor position.
    QTextCursor cursor = cursorForPosition(event->pos());

    int lineno = cursor.blockNumber()+1;

    // If there is a breakpoint on the line, toggle it.
    if (hasBreakpointLine(lineno)) {

        // Toggle the breakpoint.
        // Enable if disabled. Disable if enabled.
        if (breakpointLineEnabled(lineno) == false) {
            // Emit the enable breakpoint signal.
            emit enableBreakpoints(QString("%1").arg(breakpointLineToNumber(lineno)));
        }else{
            // Emit the disable breakpoint signal.
            emit deleteBreakpoints(QString("%1").arg(breakpointLineToNumber(lineno)));
        }

    // Otherwise, do a quick create of a new breakpoint.
    }else{
        emit insertBreakpoint(QString("-f --source \"%1\" --line %2").arg(fullname()).arg(lineno));
    }
}

void SeerEditorWidgetSourceArea::setQuickRunToLine (QMouseEvent* event) {

    // Get the line number for the cursor position.
    QTextCursor cursor = cursorForPosition(event->pos());

    int lineno = cursor.blockNumber()+1;

    // Emit the runToLine signal.
    emit runToLine(fullname(), lineno);

    return;
}

void SeerEditorWidgetSourceArea::showBreakpointToolTip (QMouseEvent* event) {

    // Get the line number for the cursor position.
    QTextCursor cursor = cursorForPosition(event->pos());

    int lineno = cursor.blockNumber()+1;

    // If there is a breakpoint on the line, ask for its information.
    if (hasBreakpointLine(lineno)) {

        _selectedBreakpointPosition = event->pos();

        // Emit the info breakpoint signal.
        emit infoBreakpoint(_selectedBreakpointId, QString("%1").arg(breakpointLineToNumber(lineno)));

    }else{
        _selectedBreakpointPosition = QPoint();
    }
}

void SeerEditorWidgetSourceArea::clearExpression() {

    _selectedExpressionCursor   = QTextCursor();
    _selectedExpressionPosition = QPoint();
    _selectedExpressionName     = "";
    _selectedExpressionValue    = "";
    _selectedBreakpointPosition = QPoint();
}

void SeerEditorWidgetSourceArea::setHighlighterSettings (const SeerHighlighterSettings& settings) {

    _sourceHighlighterSettings = settings;

    emit highlighterSettingsChanged();
}

const SeerHighlighterSettings& SeerEditorWidgetSourceArea::highlighterSettings () const {

    return _sourceHighlighterSettings;
}

void SeerEditorWidgetSourceArea::setHighlighterEnabled (bool flag) {

    _sourceHighlighterEnabled = flag;

    emit highlighterSettingsChanged();
}

bool SeerEditorWidgetSourceArea::highlighterEnabled () const {

    return _sourceHighlighterEnabled;
}

void SeerEditorWidgetSourceArea::setEditorFont (const QFont& font) {

    setFont(font);

    invalidateMiniMapCache();

    /*

     * None of the tabstops work. Setting tabstops never look the same
     * as replacing tabs with spaces. Not sure why.
     * Tried setTabStopDistance() and setTabArray(). Same problem.
     * Zooming in and out in QPlainTextEdit will also result in bad
     * tabstops.

    QFont f = font;

    f.setLetterSpacing(QFont::PercentageSpacing, 100);
    f.setWordSpacing(0.0);
    f.setFixedPitch(true);
    f.setKerning(false);

    setFont(f);

    qreal tabspace = fontMetrics().horizontalAdvance(QLatin1Char(' ')) * 4;

  //setTabStopDistance(tabspace);

    QList<qreal> tabstops;
    qreal        tabpos = 0.0;

    for (int i=0; i<100; i++) {
        tabstops.append(tabpos);
        tabpos += tabspace;
    }

    QTextOption opt = document()->defaultTextOption();
    opt.setFlags(QTextOption::ShowTabsAndSpaces);
    opt.setTabArray(tabstops);
    document()->setDefaultTextOption(opt);

    */
}

const QFont& SeerEditorWidgetSourceArea::editorFont () const {

    return font();
}

void SeerEditorWidgetSourceArea::setEditorTabSize (int spaces) {

    _sourceTabSize = spaces;
}

int SeerEditorWidgetSourceArea::editorTabSize () const {

    return _sourceTabSize;
}

void SeerEditorWidgetSourceArea::setExternalEditorCommand (const QString& externalEditorCommand) {

    _externalEditorCommand = externalEditorCommand;
}

const QString& SeerEditorWidgetSourceArea::externalEditorCommand () {

    return _externalEditorCommand;
}

void SeerEditorWidgetSourceArea::setAutoSourceReload (bool flag) {

    _autoSourceReload = flag;
}

bool SeerEditorWidgetSourceArea::autoSourceReload () const {

    return _autoSourceReload;
}

void SeerEditorWidgetSourceArea::eraseColorCurrentLine (int lineno) {

    // Erase color of this line
    QTextCharFormat lineFormat;
    lineFormat = highlighterSettings().get("Text");

    // Create a selection at the cursor.
    QTextBlock  block  = document()->findBlockByLineNumber(lineno-1);
    QTextCursor cursor = textCursor();

    cursor.setPosition(block.position());

    QTextEdit::ExtraSelection selection;
    selection.format.setForeground(lineFormat.foreground());
    selection.format.setBackground(lineFormat.background());
    selection.format.setProperty(QTextFormat::FullWidthSelection, true);
    selection.cursor = cursor;
    selection.cursor.clearSelection();

    // Add it to the extra selection list.
    _currentLinesExtraSelections.append(selection);

    // Refresh all the extra selections.
    refreshExtraSelections();
}

// Read current position in the source area: file name, line, column of cursor and first displayed line
SeerEditorWidgetSourceArea::SeerCurrentFile SeerEditorWidgetSourceArea::readCurrentPosition() {

    SeerCurrentFile info;
    info.file               = QFileInfo(file()).fileName();     // extract file name from full path
    info.fullname           = fullname();
    info.cursorRow          = currentLine();
    info.cursorCol          = currentColumn();
    info.firstDisplayLine   = firstDisplayLine();

    return info;
}

void SeerEditorWidgetSourceArea::handleText (const QString& text) {

    if (text.startsWith("*stopped")) {

        // *stopped,
        //
        // reason="end-stepping-range",
        //
        // frame={addr="0x0000000000400b45",
        //        func="main",
        //        args=[{name="argc",value="1"},{name="argv",value="0x7fffffffd5b8"}],
        //        file="helloworld.cpp",
        //        fullname="/home/erniep/Development/Peak/src/Seer/helloworld/helloworld.cpp",
        //        line="7",
        //        arch="i386:x86-64"},
        //
        // thread-id="1",
        // stopped-threads="all",
        // core="6"

        QString newtext = Seer::filterEscapes(text); // Filter escaped characters.

        QString frame_text = Seer::parseFirst(newtext, "frame=", '{', '}', false);

        if (frame_text == "") {
            return;
        }

        QString fullname_text = Seer::parseFirst(frame_text, "fullname=", '"', '"', false);
        QString file_text     = Seer::parseFirst(frame_text, "file=",     '"', '"', false);
        QString line_text     = Seer::parseFirst(frame_text, "line=",     '"', '"', false);

        // Read the file if it hasn't been read before or if we are reading a different file.
        if (fullname_text != fullname()) {
            open(fullname_text, QFileInfo(file_text).fileName());
        }

        // Move to the line number.
        setCurrentLine(line_text.toInt());

        return;

    }else if (text.contains(QRegularExpression("^([0-9]+)\\^done,value="))) {

        // 10^done,value="1"
        // 11^done,value="0x7fffffffd538"

        QString id_text = text.section('^', 0,0);

        if (id_text.toInt() == _selectedExpressionId) {

            _selectedExpressionValue = Seer::filterEscapes(Seer::parseFirst(text, "value=", '"', '"', false));

            showExpressionTooltip();
        }

        return;

    }else if (text.contains(QRegularExpression("^([0-9]+)\\^done,BreakpointTable="))) {

        // 11^done,BreakpointTable={...}

        QString id_text = text.section('^', 0,0);

        if (id_text.toInt() == _selectedBreakpointId && _selectedBreakpointPosition != QPoint()) {
            handleBreakpointToolTip(_selectedBreakpointPosition, text);
        }

        return;

    }else if (text.contains(QRegularExpression("^([0-9]+)\\^error,msg="))) {

        // 12^error,msg="No symbol \"return\" in current context."
        // 13^error,msg="No symbol \"cout\" in current context."

        QString id_text = text.section('^', 0,0);

        if (id_text.toInt() == _selectedExpressionId) {

            _selectedExpressionValue = Seer::filterEscapes(Seer::parseFirst(text, "msg=", '"', '"', false));

            // Refresh the tooltip event.
            QHelpEvent* event = new QHelpEvent(QEvent::ToolTip, _selectedExpressionPosition, this->mapToGlobal(_selectedExpressionPosition));

            QCoreApplication::postEvent(this, event);
        }

        if (id_text.toInt() == _selectedBreakpointId && _selectedBreakpointPosition != QPoint()) {
            qDebug() << "Error displaying breakpoint info as a ToolTip";
        }

        return;

    }else{
        // Ignore others.
        return;
    }
}

void SeerEditorWidgetSourceArea::handleHighlighterSettingsChanged () {

    // Set base color for background and text color.
    // Use the palette to do this. Some people say to use the stylesheet.
    // But the palettle method works (for now).
    QTextCharFormat format = highlighterSettings().get("Text");

    QPalette p = palette();
    p.setColor(QPalette::Base, format.background().color());
    p.setColor(QPalette::Text, format.foreground().color());
    setPalette(p);

    // Update the syntax highlighter.
    if (_sourceHighlighter) {
        delete _sourceHighlighter; _sourceHighlighter = 0;
    }

    SeerSourceHighlighter* highlighter = SeerSourceHighlighter::getSourceHighlighter(_file, _sourceHighlighterSettings);

    if (highlighter) {

        _sourceHighlighter = highlighter;

        if (highlighterEnabled()) {
            _sourceHighlighter->setDocument(document());
        }else{
            _sourceHighlighter->setDocument(0);
        }

        _sourceHighlighter->setHighlighterSettings(highlighterSettings());
        _sourceHighlighter->rehighlight();
    }

    // Note. The margins are automatically updated by their own paint events.
    //       The new highlighter settings will be used.
    invalidateMiniMapCache();
}

void SeerEditorWidgetSourceArea::handleWatchFileModified (const QString& path) {

    Q_UNUSED(path);

    emit showReloadBar(true);
}

void SeerEditorWidgetSourceArea::handleBreakpointToolTip (QPoint pos, const QString& text) {

    // 11^7^done,BreakpointTable={...}
    // 7^done,BreakpointTable={
    //                          nr_rows="1",
    //                          nr_cols="6",
    //                          hdr=[],
    //                          body=[
    //                                 bkpt={
    //                                        number="2",
    //                                        type="breakpoint",
    //                                        disp="keep",
    //                                        enabled="y",
    //                                        addr="0x0000000000400ccd",
    //                                        func="main(int, char**)",
    //                                        file="helloworld.cpp",
    //                                        fullname="/nas/erniep/Development/seer/tests/helloworld/helloworld.cpp",
    //                                        line="30",
    //                                        thread-groups=["i1"],
    //                                        times="1",
    //                                        original-location="-source /nas/erniep/Development/seer/tests/helloworld/helloworld.cpp -line 30"
    //                                      }
    //                               ]
    //                        }

    QString newtext = Seer::filterEscapes(text); // Filter escaped characters.
    QString tooltiptext;

    //
    // Parse 'body' text.
    //
    QString body_text = Seer::parseFirst(newtext, "body=", '[', ']', false);

    if (body_text != "") {

        QStringList bkpt_list = Seer::parse(body_text, "bkpt=", '{', '}', false);

        // Construct the tooltip.
        tooltiptext += "Breakpoint information\n\n";
        for (const auto& bkpt_text : bkpt_list) {

            QStringList item_list = Seer::parseCommaList(bkpt_text);

            for (const auto& item_text : item_list) {
                tooltiptext += item_text + "\n";
            }

            break; // Take the first breakpoint in case, somehow, more are returned.
        }
    }

    QToolTip::showText(mapToGlobal(pos), tooltiptext);
}

//
// LineNumber area.
//

SeerEditorWidgetSourceLineNumberArea::SeerEditorWidgetSourceLineNumberArea(SeerEditorWidgetSourceArea* editorWidget) : QWidget(editorWidget) {
    _editorWidget = editorWidget;
}

QSize SeerEditorWidgetSourceLineNumberArea::sizeHint () const {
    return QSize(_editorWidget->lineNumberAreaWidth(), 0);
}

void SeerEditorWidgetSourceLineNumberArea::paintEvent (QPaintEvent* event) {
    _editorWidget->lineNumberAreaPaintEvent(event);
}

void SeerEditorWidgetSourceLineNumberArea::mouseDoubleClickEvent (QMouseEvent* event) {

    if (event->button() == Qt::LeftButton) {
        if (QApplication::keyboardModifiers().testFlag(Qt::ControlModifier) == true) {
            _editorWidget->setQuickRunToLine(event);
        }else{
            _editorWidget->setQuickBreakpoint(event);
        }

    }else{
        QWidget::mouseDoubleClickEvent(event);
    }
}

void SeerEditorWidgetSourceLineNumberArea::mouseMoveEvent (QMouseEvent* event) {

    QWidget::mouseMoveEvent(event);
}

void SeerEditorWidgetSourceLineNumberArea::mousePressEvent (QMouseEvent* event) {

    if (event->button() == Qt::RightButton) {
        _editorWidget->showContextMenu(event);

    }else{
        QWidget::mousePressEvent(event);
    }

}

void SeerEditorWidgetSourceLineNumberArea::mouseReleaseEvent (QMouseEvent* event) {

    QWidget::mouseReleaseEvent(event);
}

//
// Breakpoints Area.
//

SeerEditorWidgetSourceBreakPointArea::SeerEditorWidgetSourceBreakPointArea(SeerEditorWidgetSourceArea* editorWidget) : QWidget(editorWidget) {
    _editorWidget = editorWidget;
}

QSize SeerEditorWidgetSourceBreakPointArea::sizeHint () const {
    return QSize(_editorWidget->breakPointAreaWidth(), 0);
}

void SeerEditorWidgetSourceBreakPointArea::paintEvent (QPaintEvent* event) {
    _editorWidget->breakPointAreaPaintEvent(event);
}

void SeerEditorWidgetSourceBreakPointArea::mouseDoubleClickEvent (QMouseEvent* event) {

    if (event->button() == Qt::LeftButton) {

        if (QApplication::keyboardModifiers().testFlag(Qt::ControlModifier) == true) {
            _editorWidget->setQuickRunToLine(event);
        }else{
            _editorWidget->setQuickBreakpoint(event);
        }

    }else{
        QWidget::mouseDoubleClickEvent(event);
    }
}

void SeerEditorWidgetSourceBreakPointArea::mouseMoveEvent (QMouseEvent* event) {

    QWidget::mouseMoveEvent(event);
}

void SeerEditorWidgetSourceBreakPointArea::mousePressEvent (QMouseEvent* event) {

    if (event->button() == Qt::RightButton) {
        _editorWidget->showContextMenu(event);
    }else if (event->button() == Qt::LeftButton) {
        _editorWidget->showBreakpointToolTip(event);
    }else{
        QWidget::mousePressEvent(event);
    }
}

void SeerEditorWidgetSourceBreakPointArea::mouseReleaseEvent (QMouseEvent* event) {

    QWidget::mouseReleaseEvent(event);
}

SeerEditorWidgetSourceMiniMapArea::SeerEditorWidgetSourceMiniMapArea(SeerEditorWidgetSourceArea* editorWidget) : QWidget(editorWidget) {
    _editorWidget = editorWidget;
    _dragging     = false;
}

QSize SeerEditorWidgetSourceMiniMapArea::sizeHint () const {
    return QSize(_editorWidget->miniMapAreaWidth(), 0);
}

void SeerEditorWidgetSourceMiniMapArea::paintEvent (QPaintEvent* event) {
    _editorWidget->miniMapAreaPaintEvent(event);
}

void SeerEditorWidgetSourceMiniMapArea::mousePressEvent (QMouseEvent* event) {

    if (event->button() == Qt::LeftButton) {
        _dragging = true;
        _editorWidget->jumpToMiniMapY(event->pos().y());
    }else{
        QWidget::mousePressEvent(event);
    }
}

void SeerEditorWidgetSourceMiniMapArea::mouseMoveEvent (QMouseEvent* event) {

    if (_dragging && (event->buttons() & Qt::LeftButton)) {
        _editorWidget->jumpToMiniMapY(event->pos().y());
    }else{
        QWidget::mouseMoveEvent(event);
    }
}

void SeerEditorWidgetSourceMiniMapArea::mouseReleaseEvent (QMouseEvent* event) {

    _dragging = false;

    QWidget::mouseReleaseEvent(event);
}

/***********************************************************************************************************************
 *  Functions for mouse navigation feature                                                                             *
 **********************************************************************************************************************/
void SeerEditorWidgetSourceArea::mousePressEvent(QMouseEvent *event)
{
    // This part is for mouse navigation feature 
    if (event->button() == Qt::XButton1 || event->button() == Qt::XButton2) {
        // Avoid the default back/forward action
        _ignoreThumbMouseEvent ++;
        SeerCurrentFile firstInfo = readCurrentPosition();
        // _ignoreThumbMouseEvent will incremented in mousePressEvent when thumb mouse button is pressed but it may not be decremented
        // if user just click the thumb mouse button without moving the cursor. So here we check if the position is the same as last time, 
        // which means cursor didn't move, then we reset _ignoreThumbMouseEvent to 0 to avoid blocking future events.
        QTimer::singleShot(30, this, [this, firstInfo]() {      // let's give 30 ms for cursor to move to new position
            SeerCurrentFile secondInfo = readCurrentPosition();
            if (firstInfo == secondInfo) {                      // cursor didn't move
                _ignoreThumbMouseEvent = 0;                     // reset to 0
            }
        } );
    }
    // This part is for Go to definition (Ctrl + Click) feature
    if (event->button() == Qt::LeftButton && _altHeld) {
        if (_wordUnderCursor != "")
        {
            QApplication::restoreOverrideCursor();
            signalGotoDefinition(_wordUnderCursor);
            event->ignore();        // If we don't ignore the event, the cursor will move to that position, which is not desired
            return;
        }
    }
    QPlainTextEdit::mousePressEvent(event);
}

void SeerEditorWidgetSourceArea::handleCursorPositionChanged()
{
    // Emit signal to SeerEditorManagerWidget.cpp and save position
    // Read current position, deploy a timer
    SeerCurrentFile firstInfo = readCurrentPosition();
    if (_ignoreThumbMouseEvent > 0)
    {
        _ignoreThumbMouseEvent --;
        return;
    }

    QTimer::singleShot(2000, this, [this, firstInfo]() {
        SeerCurrentFile secondInfo = readCurrentPosition();
        if (firstInfo == secondInfo) {
            emit addToMouseNavigation(firstInfo);
        }
    } );
}

/***********************************************************************************************************************
 * Go to definition (F12) feature                                                                                      *
 **********************************************************************************************************************/
void SeerEditorWidgetSourceArea::keyPressEvent(QKeyEvent *event)
{
    if (event->key() == Qt::Key_Alt)
    {
        _altHeld = true;
    }
    QPlainTextEdit::keyPressEvent(event);
}

void SeerEditorWidgetSourceArea::keyReleaseEvent(QKeyEvent *event)
{
    if (event->key() == Qt::Key_Alt)
    {
        _altHeld = false;
    }
    QPlainTextEdit::keyReleaseEvent(event);
}

bool SeerEditorWidgetSourceArea::isOverWord(const QPoint &pos)
{
    QTextCursor cursor = cursorForPosition(pos);
    cursor.select(QTextCursor::WordUnderCursor);
    return !cursor.selectedText().isEmpty();
}

QString SeerEditorWidgetSourceArea::wordUnderCursor(const QPoint &pos) const
{
    int leftMarginOffset = 0;
    QMargins margins = viewportMargins();
    QPoint adjustedPos = pos;

    // The correct position to get the word under cursor should subtract the left margin 
    // offset (breakpoint area and line number area)
    leftMarginOffset = margins.left();
    adjustedPos.setX(pos.x() - leftMarginOffset);
    QTextCursor cursor = cursorForPosition(adjustedPos);
    cursor.select(QTextCursor::WordUnderCursor);
    return cursor.selectedText();
}

void SeerEditorWidgetSourceArea::updateCursor(const QPoint &pos)
{
    // Why not QApplication::setOverrideCursor()? Because QApplication::setOverrideCursor uses internal stack
    // It causes delay and sometimes the cursor won't change back to normal when we want it to. 
    // In short, QApplication::setOverrideCursor is global and not real time
    // In contrary, viewport()->setCursor() is local and real time, apply only for that widget, in this case, the text area 
    QPoint localPos = mapFromGlobal(pos);
    if (!_altHeld) {
        viewport()->setCursor(Qt::IBeamCursor);
        _wordUnderCursor = "";
        return;
    }
    if (!hasFocus())
        return;
    _wordUnderCursor = wordUnderCursor(localPos);
    if (isValidIdentifier(_wordUnderCursor))
    {
        viewport()->setCursor(Qt::PointingHandCursor);
    } else {
        viewport()->setCursor(Qt::IBeamCursor);
        _wordUnderCursor = "";
    }
}

// Check text and decide if that text is valid identifier (function, variable, type name)
bool SeerEditorWidgetSourceArea::isValidIdentifier(const QString& text) 
{
    static const QSet<QString> keywords = {
        // Add your C and C++ keywords as QString literals here
        "auto", "break", "case", "char", "const", "continue", "default", "do", "double",
        "else", "enum", "extern", "float", "for", "goto", "if", "inline", "int", "long",
        "register", "restrict", "return", "short", "signed", "sizeof", "static", "struct",
        "switch", "typedef", "union", "unsigned", "void", "volatile", "while", "_Alignas",
        "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", "_Noreturn",
        "_Static_assert", "_Thread_local",

        "alignas", "alignof", "and", "and_eq", "asm", "bitand", "bitor", "bool", "catch",
        "char16_t", "char32_t", "class", "compl", "const_cast", "constexpr", "decltype",
        "delete", "dynamic_cast", "explicit", "export", "false", "friend", "mutable", "namespace",
        "new", "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", "private",
        "protected", "public", "reinterpret_cast", "static_assert", "static_cast", "template",
        "this", "thread_local", "throw", "true", "try", "typeid", "typename", "using",
        "virtual", "wchar_t", "xor", "xor_eq"
    };

    if (text.isEmpty())
        return false;

    if (keywords.contains(text))
        return false;

    QChar firstChar = text[0];
    if (!firstChar.isLetter() && firstChar != '_')
        return false;

    for (int i = 1; i < text.size(); ++i) {
        QChar ch = text[i];
        if (!ch.isLetterOrNumber() && ch != '_')
            return false;
    }

    return true;
}

// When F12 is pressed, try to look for the word under cursor, if it's a valid identifier then emit signalGotoDefinition
void SeerEditorWidgetSourceArea::handleGotoDefinition() {

    QTextCursor cursor = textCursor();
    cursor.select(QTextCursor::WordUnderCursor);
    QString wordUnderCursor = cursor.selectedText();

    if (isValidIdentifier(wordUnderCursor)) {
        emit signalGotoDefinition(wordUnderCursor);
    }
}

