Skip to content
Snippets Groups Projects
basetexteditor.cpp 198 KiB
Newer Older
/**************************************************************************
con's avatar
con committed
**
** This file is part of Qt Creator
**
hjk's avatar
hjk committed
** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
con's avatar
con committed
**
** Contact: Nokia Corporation (qt-info@nokia.com)
con's avatar
con committed
**
** Commercial Usage
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
** If you are unsure which license is appropriate for your use, please
hjk's avatar
hjk committed
** contact the sales department at http://qt.nokia.com/contact.
con's avatar
con committed
**
**************************************************************************/
hjk's avatar
hjk committed

con's avatar
con committed
#include "texteditor_global.h"
hjk's avatar
hjk committed

con's avatar
con committed
#include "basetextdocument.h"
#include "basetexteditor_p.h"
#include "behaviorsettings.h"
con's avatar
con committed
#include "codecselector.h"
#include "completionsupport.h"
#include "tabsettings.h"
#include "texteditorconstants.h"
#include "texteditorplugin.h"
con's avatar
con committed

#include <aggregation/aggregate.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/actionmanager/actioncontainer.h>
#include <coreplugin/actionmanager/command.h>
con's avatar
con committed
#include <coreplugin/coreconstants.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/manhattanstyle.h>
#include <extensionsystem/pluginmanager.h>
con's avatar
con committed
#include <find/basetextfind.h>
#include <utils/linecolumnlabel.h>
hjk's avatar
hjk committed
#include <utils/qtcassert.h>
#include <utils/stylehelper.h>
con's avatar
con committed

#include <QtCore/QCoreApplication>
#include <QtCore/QTextCodec>
#include <QtCore/QFile>
#include <QtCore/QDebug>
#include <QtCore/QTimer>
#include <QtCore/QTimeLine>
con's avatar
con committed
#include <QtGui/QAbstractTextDocumentLayout>
#include <QtGui/QApplication>
#include <QtGui/QKeyEvent>
#include <QtGui/QLabel>
#include <QtGui/QLayout>
#include <QtGui/QPainter>
#include <QtGui/QPrinter>
#include <QtGui/QPrintDialog>
#include <QtGui/QScrollBar>
#include <QtGui/QShortcut>
#include <QtGui/QStyle>
#include <QtGui/QSyntaxHighlighter>
#include <QtGui/QTextCursor>
#include <QtGui/QTextDocumentFragment>
con's avatar
con committed
#include <QtGui/QTextBlock>
#include <QtGui/QTextLayout>
#include <QtGui/QToolBar>
#include <QtGui/QToolTip>
#include <QtGui/QInputDialog>
con's avatar
con committed

using namespace TextEditor;
using namespace TextEditor::Internal;

mae's avatar
mae committed
namespace {
    class Locker {
        bool *m_bool;
    public:
        inline Locker(bool *b):m_bool(b){ *m_bool = true; }
        inline ~Locker() { *m_bool = false; }
    };
}


con's avatar
con committed

namespace TextEditor {
con's avatar
con committed

class TextEditExtraArea : public QWidget {
    BaseTextEditor *textEdit;
public:
    TextEditExtraArea(BaseTextEditor *edit):QWidget(edit) {
        textEdit = edit;
        setAutoFillBackground(true);
    }
public:

    QSize sizeHint() const {
        return QSize(textEdit->extraAreaWidth(), 0);
    }
protected:
    void paintEvent(QPaintEvent *event){
        textEdit->extraAreaPaintEvent(event);
    }
    void mousePressEvent(QMouseEvent *event){
        textEdit->extraAreaMouseEvent(event);
    }
    void mouseMoveEvent(QMouseEvent *event){
        textEdit->extraAreaMouseEvent(event);
    }
    void mouseReleaseEvent(QMouseEvent *event){
        textEdit->extraAreaMouseEvent(event);
    }
    void leaveEvent(QEvent *event){
        textEdit->extraAreaLeaveEvent(event);
    }

    void wheelEvent(QWheelEvent *event) {
        QCoreApplication::sendEvent(textEdit->viewport(), event);
    }
};

} // namespace Internal
} // namespace TextEditor
con's avatar
con committed

con's avatar
con committed
ITextEditor *BaseTextEditor::openEditorAt(const QString &fileName,
                                          int line,
                                          int column,
                                          const QString &editorKind,
                                          bool *newEditor)
con's avatar
con committed
{
    Core::EditorManager *editorManager = Core::EditorManager::instance();
    editorManager->addCurrentPositionToNavigationHistory();
    Core::IEditor *editor = editorManager->openEditor(fileName, editorKind,
            Core::EditorManager::IgnoreNavigationHistory, newEditor);
con's avatar
con committed
    TextEditor::ITextEditor *texteditor = qobject_cast<TextEditor::ITextEditor *>(editor);
    if (texteditor) {
        texteditor->gotoLine(line, column);
        return texteditor;
    }
con's avatar
con committed
    return 0;
}

static void convertToPlainText(QString &txt)
{
    QChar *uc = txt.data();
    QChar *e = uc + txt.size();

    for (; uc != e; ++uc) {
        switch (uc->unicode()) {
        case 0xfdd0: // QTextBeginningOfFrame
        case 0xfdd1: // QTextEndOfFrame
        case QChar::ParagraphSeparator:
        case QChar::LineSeparator:
            *uc = QLatin1Char('\n');
            break;
        case QChar::Nbsp:
            *uc = QLatin1Char(' ');
            break;
        default:
            ;
        }
    }
}

con's avatar
con committed
BaseTextEditor::BaseTextEditor(QWidget *parent)
    : QPlainTextEdit(parent)
{
    d = new BaseTextEditorPrivate();
    d->q = this;
    d->m_extraArea = new TextEditExtraArea(this);
    d->m_extraArea->setMouseTracking(true);
    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);

    d->m_overlay = new TextEditorOverlay(this);
    d->m_snippetOverlay = new TextEditorOverlay(this);
    d->m_searchResultOverlay = new TextEditorOverlay(this);

con's avatar
con committed
    d->setupDocumentSignals(d->m_document);
    d->setupDocumentSignals(d->m_document);

    d->m_lastScrollPos = -1;
    setCursorWidth(2);

    d->m_allowSkippingOfBlockEnd = false;
con's avatar
con committed

    // from RESEARCH

con's avatar
con committed
    setLayoutDirection(Qt::LeftToRight);
    viewport()->setMouseTracking(true);
    d->extraAreaSelectionAnchorBlockNumber
        = d->extraAreaToggleMarkBlockNumber
        = d->extraAreaHighlightCollapseBlockNumber
        = d->extraAreaHighlightCollapseColumn
con's avatar
con committed
        = -1;

    d->visibleCollapsedBlockNumber = d->suggestedVisibleCollapsedBlockNumber = -1;

    connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(slotUpdateExtraAreaWidth()));
    connect(this, SIGNAL(modificationChanged(bool)), this, SLOT(slotModificationChanged(bool)));
    connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(slotCursorPositionChanged()));
    connect(this, SIGNAL(updateRequest(QRect, int)), this, SLOT(slotUpdateRequest(QRect, int)));
    connect(this, SIGNAL(selectionChanged()), this, SLOT(slotSelectionChanged()));

//     (void) new QShortcut(tr("CTRL+L"), this, SLOT(centerCursor()), 0, Qt::WidgetShortcut);
//     (void) new QShortcut(tr("F9"), this, SLOT(slotToggleMark()), 0, Qt::WidgetShortcut);
//     (void) new QShortcut(tr("F11"), this, SLOT(slotToggleBlockVisible()));


    // parentheses matcher
    d->m_parenthesesMatchingEnabled = false;
    d->m_formatRange = true;
    d->m_matchFormat.setForeground(Qt::red);
    d->m_rangeFormat.setBackground(QColor(0xb4, 0xee, 0xb4));
    d->m_mismatchFormat.setBackground(Qt::magenta);
    d->m_parenthesesMatchingTimer = new QTimer(this);
    d->m_parenthesesMatchingTimer->setSingleShot(true);
    connect(d->m_parenthesesMatchingTimer, SIGNAL(timeout()), this, SLOT(_q_matchParentheses()));

    d->m_highlightBlocksTimer = new QTimer(this);
    d->m_highlightBlocksTimer->setSingleShot(true);
    connect(d->m_highlightBlocksTimer, SIGNAL(timeout()), this, SLOT(_q_highlightBlocks()));

con's avatar
con committed

    d->m_searchResultFormat.setBackground(QColor(0xffef0b));

    slotUpdateExtraAreaWidth();
con's avatar
con committed
    updateHighlights();
con's avatar
con committed
    setFrameStyle(QFrame::NoFrame);

    d->m_delayedUpdateTimer = new QTimer(this);
    d->m_delayedUpdateTimer->setSingleShot(true);
    connect(d->m_delayedUpdateTimer, SIGNAL(timeout()), viewport(), SLOT(update()));

con's avatar
con committed
    connect(Core::EditorManager::instance(), SIGNAL(currentEditorChanged(Core::IEditor*)),
            this, SLOT(currentEditorChanged(Core::IEditor*)));
mae's avatar
mae committed

    d->m_inKeyPressEvent = false;
    d->m_moveLineUndoHack = false;
con's avatar
con committed
}

BaseTextEditor::~BaseTextEditor()
{
    delete d;
    d = 0;
}

QString BaseTextEditor::mimeType() const
{
    return d->m_document->mimeType();
}

void BaseTextEditor::setMimeType(const QString &mt)
{
    d->m_document->setMimeType(mt);
}

void BaseTextEditor::print(QPrinter *printer)
{
    const bool oldFullPage =  printer->fullPage();
    printer->setFullPage(true);
    QPrintDialog *dlg = new QPrintDialog(printer, this);
    dlg->setWindowTitle(tr("Print Document"));
    if (dlg->exec() == QDialog::Accepted) {
        d->print(printer);
    }
    printer->setFullPage(oldFullPage);
    delete dlg;
}

static int collapseBoxWidth(const QFontMetrics &fm)
{
    const int lineSpacing = fm.lineSpacing();
    return lineSpacing + lineSpacing%2 + 1;
}

con's avatar
con committed
static void printPage(int index, QPainter *painter, const QTextDocument *doc,
                      const QRectF &body, const QRectF &titleBox,
                      const QString &title)
{
    painter->save();

    painter->translate(body.left(), body.top() - (index - 1) * body.height());
    QRectF view(0, (index - 1) * body.height(), body.width(), body.height());

    QAbstractTextDocumentLayout *layout = doc->documentLayout();
    QAbstractTextDocumentLayout::PaintContext ctx;

    painter->setFont(QFont(doc->defaultFont()));
    QRectF box = titleBox.translated(0, view.top());
    int dpix = painter->device()->logicalDpiX();
    int dpiy = painter->device()->logicalDpiY();
    int mx = 5 * dpix / 72.0;
    int my = 2 * dpiy / 72.0;
    painter->fillRect(box.adjusted(-mx, -my, mx, my), QColor(210, 210, 210));
    if (!title.isEmpty())
        painter->drawText(box, Qt::AlignCenter, title);
    const QString pageString = QString::number(index);
    painter->drawText(box, Qt::AlignRight, pageString);

    painter->setClipRect(view);
    ctx.clip = view;
    // don't use the system palette text as default text color, on HP/UX
    // for example that's white, and white text on white paper doesn't
    // look that nice
    ctx.palette.setColor(QPalette::Text, Qt::black);

    layout->draw(painter, ctx);

    painter->restore();
}

void BaseTextEditorPrivate::print(QPrinter *printer)
{

    QTextDocument *doc = q->document();

    QString title = q->displayName();
    if (title.isEmpty())
        printer->setDocName(title);


    QPainter p(printer);

    // Check that there is a valid device to print to.
    if (!p.isActive())
        return;

    doc = doc->clone(doc);

    QTextOption opt = doc->defaultTextOption();
    opt.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
    doc->setDefaultTextOption(opt);

    (void)doc->documentLayout(); // make sure that there is a layout


    QColor background = q->palette().color(QPalette::Base);
    bool backgroundIsDark = background.value() < 128;

    for (QTextBlock srcBlock = q->document()->firstBlock(), dstBlock = doc->firstBlock();
         srcBlock.isValid() && dstBlock.isValid();
         srcBlock = srcBlock.next(), dstBlock = dstBlock.next()) {


        QList<QTextLayout::FormatRange> formatList = srcBlock.layout()->additionalFormats();
        if (backgroundIsDark) {
            // adjust syntax highlighting colors for better contrast
            for (int i = formatList.count() - 1; i >=0; --i) {
                QTextCharFormat &format = formatList[i].format;
                if (format.background().color() == background) {
                    QBrush brush = format.foreground();
                    QColor color = brush.color();
                    int h,s,v,a;
                    color.getHsv(&h, &s, &v, &a);
                    color.setHsv(h, s, qMin(128, v), a);
                    brush.setColor(color);
                    format.setForeground(brush);
                }
                format.setBackground(Qt::white);
            }
        }

        dstBlock.layout()->setAdditionalFormats(formatList);
    }

    QAbstractTextDocumentLayout *layout = doc->documentLayout();
    layout->setPaintDevice(p.device());

    int dpiy = p.device()->logicalDpiY();
    int margin = (int) ((2/2.54)*dpiy); // 2 cm margins

    QTextFrameFormat fmt = doc->rootFrame()->frameFormat();
    fmt.setMargin(margin);
    doc->rootFrame()->setFrameFormat(fmt);

    QRectF pageRect(printer->pageRect());
    QRectF body = QRectF(0, 0, pageRect.width(), pageRect.height());
    QFontMetrics fontMetrics(doc->defaultFont(), p.device());

    QRectF titleBox(margin,
                    body.top() + margin
                    - fontMetrics.height()
                    - 6 * dpiy / 72.0,
                    body.width() - 2*margin,
                    fontMetrics.height());
    doc->setPageSize(body.size());

    int docCopies;
    int pageCopies;
    if (printer->collateCopies() == true){
        docCopies = 1;
        pageCopies = printer->numCopies();
    } else {
        docCopies = printer->numCopies();
        pageCopies = 1;
    }

    int fromPage = printer->fromPage();
    int toPage = printer->toPage();
    bool ascending = true;

    if (fromPage == 0 && toPage == 0) {
        fromPage = 1;
        toPage = doc->pageCount();
    }
    // paranoia check
    fromPage = qMax(1, fromPage);
    toPage = qMin(doc->pageCount(), toPage);

    if (printer->pageOrder() == QPrinter::LastPageFirst) {
        int tmp = fromPage;
        fromPage = toPage;
        toPage = tmp;
        ascending = false;
    }

    for (int i = 0; i < docCopies; ++i) {

        int page = fromPage;
        while (true) {
            for (int j = 0; j < pageCopies; ++j) {
                if (printer->printerState() == QPrinter::Aborted
                    || printer->printerState() == QPrinter::Error)
                    goto UserCanceled;
                printPage(page, &p, doc, body, titleBox, title);
                if (j < pageCopies - 1)
                    printer->newPage();
            }

            if (page == toPage)
                break;

            if (ascending)
                ++page;
            else
                --page;

            printer->newPage();
        }

        if ( i < docCopies - 1)
            printer->newPage();
    }

UserCanceled:
    delete doc;
}


bool DocumentMarker::addMark(TextEditor::ITextMark *mark, int line)
{
hjk's avatar
hjk committed
    QTC_ASSERT(line >= 1, return false);
con's avatar
con committed
    int blockNumber = line - 1;
    TextEditDocumentLayout *documentLayout = qobject_cast<TextEditDocumentLayout*>(document->documentLayout());
hjk's avatar
hjk committed
    QTC_ASSERT(documentLayout, return false);
con's avatar
con committed
    QTextBlock block = document->findBlockByNumber(blockNumber);

    if (block.isValid()) {
        TextBlockUserData *userData = TextEditDocumentLayout::userData(block);
        userData->addMark(mark);
        mark->updateLineNumber(blockNumber + 1);
        mark->updateBlock(block);
        documentLayout->hasMarks = true;
        documentLayout->requestUpdate();
        return true;
    }
    return false;
}

int BaseTextEditorPrivate::visualIndent(const QTextBlock &block) const
{
    if (!block.isValid())
        return 0;
    const QTextDocument *document = block.document();
    int i = 0;
    while (i < block.length()) {
        if (!document->characterAt(block.position() + i).isSpace()) {
            QTextCursor cursor(block);
            cursor.setPosition(block.position() + i);
            return q->cursorRect(cursor).x();
        }
        ++i;
    }

    return 0;
}
con's avatar
con committed

TextEditor::TextMarks DocumentMarker::marksAt(int line) const
{
hjk's avatar
hjk committed
    QTC_ASSERT(line >= 1, return TextMarks());
con's avatar
con committed
    int blockNumber = line - 1;
    QTextBlock block = document->findBlockByNumber(blockNumber);

    if (block.isValid()) {
        if (TextBlockUserData *userData = TextEditDocumentLayout::testUserData(block))
            return userData->marks();
    }
    return TextMarks();
}

void DocumentMarker::removeMark(TextEditor::ITextMark *mark)
{
    bool needUpdate = false;
    QTextBlock block = document->begin();
    while (block.isValid()) {
        if (TextBlockUserData *data = static_cast<TextBlockUserData *>(block.userData())) {
            needUpdate |= data->removeMark(mark);
        }
        block = block.next();
    }
    if (needUpdate)
        updateMark(0);
}


bool DocumentMarker::hasMark(TextEditor::ITextMark *mark) const
{
    QTextBlock block = document->begin();
    while (block.isValid()) {
        if (TextBlockUserData *data = static_cast<TextBlockUserData *>(block.userData())) {
            if (data->hasMark(mark))
                return true;
        }
        block = block.next();
    }
    return false;
}

ITextMarkable *BaseTextEditor::markableInterface() const
{
    return baseTextDocument()->documentMarker();
}

void BaseTextEditor::maybeEmitContentsChangedBecauseOfUndo()
mae's avatar
mae committed
    if (!d->m_inKeyPressEvent) {

        // i.e. the document was changed outside key press event
        // Possible with undo, cut, paste, etc.
        if (d->m_snippetOverlay->isVisible()) {

            d->m_snippetOverlay->hide();
            d->m_snippetOverlay->clear();
            QTextCursor cursor = textCursor();
            cursor.clearSelection();
            setTextCursor(cursor);
            return;
        }

    }
    if (document()->isRedoAvailable()) {
        emit d->m_editable->contentsChangedBecauseOfUndo();
mae's avatar
mae committed
    }
con's avatar
con committed
ITextEditable *BaseTextEditor::editableInterface() const
{
    if (!d->m_editable) {
        d->m_editable = const_cast<BaseTextEditor*>(this)->createEditableInterface();
        connect(this, SIGNAL(textChanged()),
                d->m_editable, SIGNAL(contentsChanged()));
        connect(this, SIGNAL(textChanged()),
                this, SLOT(maybeEmitContentsChangedBecauseOfUndo()));
con's avatar
con committed
        connect(this, SIGNAL(changed()),
                d->m_editable, SIGNAL(changed()));
    }
    return d->m_editable;
}


void BaseTextEditor::currentEditorChanged(Core::IEditor *editor)
{
    if (editor == d->m_editable) {
        if (d->m_document->hasDecodingError()) {
            Core::EditorManager::instance()->showEditorInfoBar(QLatin1String(Constants::SELECT_ENCODING),
                tr("<b>Error:</b> Could not decode \"%1\" with \"%2\"-encoding. Editing not possible.")
                    .arg(displayName()).arg(QString::fromLatin1(d->m_document->codec()->name())),
con's avatar
con committed
                tr("Select Encoding"),
                this, SLOT(selectEncoding()));
        }
    }
}

void BaseTextEditor::selectEncoding()
{
    BaseTextDocument *doc = d->m_document;
    CodecSelector codecSelector(this, doc);

    switch (codecSelector.exec()) {
    case CodecSelector::Reload:
        doc->reload(codecSelector.selectedCodec());
        setReadOnly(d->m_document->hasDecodingError());
        if (doc->hasDecodingError())
            currentEditorChanged(Core::EditorManager::instance()->currentEditor());
        else
            Core::EditorManager::instance()->hideEditorInfoBar(QLatin1String(Constants::SELECT_ENCODING));
        break;
    case CodecSelector::Save:
        doc->setCodec(codecSelector.selectedCodec());
        Core::EditorManager::instance()->saveEditor(editableInterface());
        break;
    case CodecSelector::Cancel:
        break;
    }
}

void DocumentMarker::updateMark(ITextMark *mark)
{
hjk's avatar
hjk committed
    TextEditDocumentLayout *documentLayout = qobject_cast<TextEditDocumentLayout*>(document->documentLayout());
    QTC_ASSERT(documentLayout, return);
con's avatar
con committed
    documentLayout->requestUpdate();
}


void BaseTextEditor::triggerCompletions()
{
    emit requestAutoCompletion(editableInterface(), true);
}

void BaseTextEditor::triggerQuickFix()
{
    emit requestQuickFix(editableInterface());
}

con's avatar
con committed
bool BaseTextEditor::createNew(const QString &contents)
{
    setPlainText(contents);
    document()->setModified(false);
    return true;
}

bool BaseTextEditor::open(const QString &fileName)
{
    if (d->m_document->open(fileName)) {
        moveCursor(QTextCursor::Start);
        setReadOnly(d->m_document->hasDecodingError());
        return true;
    }
    return false;
}

/*
  Collapses the first comment in a file, if there is only whitespace above
  */
void BaseTextEditorPrivate::collapseLicenseHeader()
{
    QTextDocument *doc = q->document();
    TextEditDocumentLayout *documentLayout = qobject_cast<TextEditDocumentLayout*>(doc->documentLayout());
    QTC_ASSERT(documentLayout, return);
    QTextBlock block = doc->firstBlock();
    const TabSettings &ts = m_document->tabSettings();
    while (block.isValid() && block.isVisible()) {
        TextBlockUserData *data = TextBlockUserData::canCollapse(block);
        if (data && block.next().isVisible()) {
            QChar character;
            static_cast<TextBlockUserData*>(data)->collapseAtPos(&character);
            if (character != QLatin1Char('+'))
                break; // not a comment
            TextBlockUserData::doCollapse(block, false);
            moveCursorVisible();
            documentLayout->requestUpdate();
            documentLayout->emitDocumentSizeChanged();
            break;
        }
        QString text = block.text();
        if (ts.firstNonSpace(text) < text.size())
            break;
        block = block.next();
    }
}

const Utils::ChangeSet &BaseTextEditor::changeSet() const
{
    return d->m_changeSet;
}

void BaseTextEditor::setChangeSet(const Utils::ChangeSet &changeSet)
{
    using namespace Utils;

    d->m_changeSet = changeSet;

    foreach (const ChangeSet::EditOp &op, changeSet.operationList()) {
        // ### TODO: process the edit operation

        switch (op.type) {
        case ChangeSet::EditOp::Replace:
            break;

        case ChangeSet::EditOp::Move:
            break;

        case ChangeSet::EditOp::Insert:
            break;

        case ChangeSet::EditOp::Remove:
            break;

        case ChangeSet::EditOp::Flip:
            break;

        case ChangeSet::EditOp::Copy:
            break;

        default:
            break;
        } // switch
    }
Core::IFile *BaseTextEditor::file()
con's avatar
con committed
{
    return d->m_document;
}

void BaseTextEditor::editorContentsChange(int position, int charsRemoved, int charsAdded)
{
    if (d->m_animator)
        d->m_animator->finish();

con's avatar
con committed
    d->m_contentsChanged = true;
    QTextDocument *doc = document();
con's avatar
con committed

    // Keep the line numbers and the block information for the text marks updated
    if (charsRemoved != 0) {
        d->updateMarksLineNumber();
        d->updateMarksBlock(document()->findBlock(position));
    } else {
        const QTextBlock posBlock = doc->findBlock(position);
        const QTextBlock nextBlock = doc->findBlock(position + charsAdded);
con's avatar
con committed
        if (posBlock != nextBlock) {
            d->updateMarksLineNumber();
            d->updateMarksBlock(posBlock);
            d->updateMarksBlock(nextBlock);
        } else {
            d->updateMarksBlock(posBlock);
        }
    }

    if (doc->isRedoAvailable())
        emit d->m_editable->contentsChangedBecauseOfUndo();
con's avatar
con committed
}


void BaseTextEditor::slotSelectionChanged()
{
    bool changed = (d->m_inBlockSelectionMode != d->m_lastEventWasBlockSelectionEvent);
    d->m_inBlockSelectionMode = d->m_lastEventWasBlockSelectionEvent;
    if (changed || d->m_inBlockSelectionMode)
        viewport()->update();
    if (!d->m_inBlockSelectionMode)
        d->m_blockSelectionExtraX = 0;
    if (!d->m_selectBlockAnchor.isNull() && !textCursor().hasSelection())
        d->m_selectBlockAnchor = QTextCursor();

    // Clear any link which might be showing when the selection changes
    clearLink();
con's avatar
con committed
}

void BaseTextEditor::gotoBlockStart()
{
    QTextCursor cursor = textCursor();
    if (TextBlockUserData::findPreviousOpenParenthesis(&cursor, false)) {
        setTextCursor(cursor);
}

void BaseTextEditor::gotoBlockEnd()
{
    QTextCursor cursor = textCursor();
    if (TextBlockUserData::findNextClosingParenthesis(&cursor, false)) {
        setTextCursor(cursor);
}

void BaseTextEditor::gotoBlockStartWithSelection()
{
    QTextCursor cursor = textCursor();
    if (TextBlockUserData::findPreviousOpenParenthesis(&cursor, true)) {
        setTextCursor(cursor);
}

void BaseTextEditor::gotoBlockEndWithSelection()
{
    QTextCursor cursor = textCursor();
    if (TextBlockUserData::findNextClosingParenthesis(&cursor, true)) {
        setTextCursor(cursor);
        _q_matchParentheses();
    }
}

static QTextCursor flippedCursor(const QTextCursor &cursor) {
    QTextCursor flipped = cursor;
    flipped.clearSelection();
    flipped.setPosition(cursor.anchor(), QTextCursor::KeepAnchor);
    return flipped;
void BaseTextEditor::selectBlockUp()
{
    QTextCursor cursor = textCursor();
    if (!cursor.hasSelection())
        d->m_selectBlockAnchor = cursor;
    else
        cursor.setPosition(cursor.selectionStart());


    if (!TextBlockUserData::findPreviousOpenParenthesis(&cursor, false))
        return;
    if (!TextBlockUserData::findNextClosingParenthesis(&cursor, true))
        return;
    setTextCursor(flippedCursor(cursor));
    _q_matchParentheses();
}

void BaseTextEditor::selectBlockDown()
{
    QTextCursor tc = textCursor();
    QTextCursor cursor = d->m_selectBlockAnchor;

    if (!tc.hasSelection() || cursor.isNull())
        return;
    tc.setPosition(tc.selectionStart());

    forever {
        QTextCursor ahead = cursor;
        if (!TextBlockUserData::findPreviousOpenParenthesis(&ahead, false))
            break;
        if (ahead.position() <= tc.position())
            break;
        cursor = ahead;
    }
    if ( cursor != d->m_selectBlockAnchor)
        TextBlockUserData::findNextClosingParenthesis(&cursor, true);

    setTextCursor(flippedCursor(cursor));
    _q_matchParentheses();
void BaseTextEditor::copyLineUp()
{
    copyLineUpDown(true);
}

void BaseTextEditor::copyLineDown()
{
    copyLineUpDown(false);
}

void BaseTextEditor::copyLineUpDown(bool up)
{
    QTextCursor cursor = textCursor();
    QTextCursor move = cursor;
    move.beginEditBlock();

    bool hasSelection = cursor.hasSelection();

    if (hasSelection) {
        move.setPosition(cursor.selectionStart());
        move.movePosition(QTextCursor::StartOfBlock);
        move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor);
        move.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
    } else {
        move.movePosition(QTextCursor::StartOfBlock);
        move.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
    }

    QString text = move.selectedText();

    if (up) {
        move.setPosition(cursor.selectionStart());
        move.movePosition(QTextCursor::StartOfBlock);
        move.insertBlock();
        move.movePosition(QTextCursor::Left);
    } else {
        move.movePosition(QTextCursor::EndOfBlock);
        if (move.atBlockStart()) {
            move.movePosition(QTextCursor::NextBlock);
            move.insertBlock();
            move.movePosition(QTextCursor::Left);
        } else {
            move.insertBlock();
        }
    }

    int start = move.position();
    move.clearSelection();
    move.insertText(text);
    int end = move.position();

    move.setPosition(start);
    move.setPosition(end, QTextCursor::KeepAnchor);

    indent(document(), move, QChar::Null);
    move.endEditBlock();

    setTextCursor(move);
}

void BaseTextEditor::joinLines()
    QTextCursor cursor = textCursor();
    QTextCursor start = cursor;
    QTextCursor end = cursor;
    start.setPosition(cursor.selectionStart());
    end.setPosition(cursor.selectionEnd() - 1);
    int lineCount = qMax(1, end.blockNumber() - start.blockNumber());
    cursor.beginEditBlock();
    cursor.setPosition(cursor.selectionStart());
    while (lineCount--) {
        cursor.movePosition(QTextCursor::NextBlock);
        cursor.movePosition(QTextCursor::StartOfBlock);
        cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
        QString cutLine = cursor.selectedText();
        // Collapse leading whitespaces to one or insert whitespace
        cutLine.replace(QRegExp("^\\s*"), " ");
        cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
        cursor.removeSelectedText();

        cursor.movePosition(QTextCursor::PreviousBlock);
        cursor.movePosition(QTextCursor::EndOfBlock);

        cursor.insertText(cutLine);
    }
    cursor.endEditBlock();

    setTextCursor(cursor);
void BaseTextEditor::moveLineUp()
{
    moveLineUpDown(true);
}

void BaseTextEditor::moveLineDown()
{
    moveLineUpDown(false);
}

void BaseTextEditor::moveLineUpDown(bool up)
{
    QTextCursor cursor = textCursor();
    QTextCursor move = cursor;

    move.setVisualNavigation(false); // this opens collapsed items instead of destroying them

    if (d->m_moveLineUndoHack)
        move.joinPreviousEditBlock();
    else
        move.beginEditBlock();

    bool hasSelection = cursor.hasSelection();

    if (cursor.hasSelection()) {
        move.setPosition(cursor.selectionStart());
        move.movePosition(QTextCursor::StartOfBlock);
        move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor);
        move.movePosition(move.atBlockStart() ? QTextCursor::Left: QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
    } else {
        move.movePosition(QTextCursor::StartOfBlock);
        move.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
    }
    QString text = move.selectedText();
    move.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
    move.removeSelectedText();
    if (up) {
        move.movePosition(QTextCursor::PreviousBlock);
        move.insertBlock();
        move.movePosition(QTextCursor::Left);
    } else {
        move.movePosition(QTextCursor::EndOfBlock);
        if (move.atBlockStart()) { // empty block
            move.movePosition(QTextCursor::NextBlock);
            move.insertBlock();
            move.movePosition(QTextCursor::Left);
        } else {
            move.insertBlock();
        }
    }
    int start = move.position();
    move.clearSelection();
    move.insertText(text);
    int end = move.position();
    if (hasSelection) {
        move.setPosition(start);
        move.setPosition(end, QTextCursor::KeepAnchor);
    }

    reindent(document(), move);
void BaseTextEditor::cleanWhitespace()
{
    d->m_document->cleanWhitespace(textCursor());
con's avatar
con committed
void BaseTextEditor::keyPressEvent(QKeyEvent *e)
{
mae's avatar
mae committed
    Locker inKeyPressEvent(&d->m_inKeyPressEvent);

    viewport()->setCursor(Qt::BlankCursor);
    QToolTip::hideText();

con's avatar
con committed
    d->clearVisibleCollapsedBlock();

    QKeyEvent *original_e = e;
    d->m_lastEventWasBlockSelectionEvent = false;

    if (e->key() == Qt::Key_Escape) {
        if (d->m_snippetOverlay->isVisible()) {
            e->accept();
            d->m_snippetOverlay->hide();
            d->m_snippetOverlay->clear();
            QTextCursor cursor = textCursor();
            cursor.clearSelection();
            setTextCursor(cursor);
            return;
        }
con's avatar
con committed
    }

    bool ro = isReadOnly();

    if (d->m_inBlockSelectionMode) {
        if (e == QKeySequence::Cut) {
            if (!ro) {
                cut();
                e->accept();
                return;
            }
        } else if (e == QKeySequence::Delete || e->key() == Qt::Key_Backspace) {
con's avatar
con committed
            if (!ro) {
                d->removeBlockSelection();
                e->accept();
                return;
            }
        } else if (e == QKeySequence::Paste) {
            if (!ro) {
                d->removeBlockSelection();
                // continue
            }
con's avatar
con committed
        }
    }


    if (!ro
        && (e == QKeySequence::InsertParagraphSeparator
            || (!d->m_lineSeparatorsAllowed && e == QKeySequence::InsertLineSeparator))
        ) {

        if (d->m_snippetOverlay->isVisible()) {
            e->accept();
            d->m_snippetOverlay->hide();
            d->m_snippetOverlay->clear();
            QTextCursor cursor = textCursor();
            cursor.movePosition(QTextCursor::EndOfBlock);
            setTextCursor(cursor);
            return;
        }


con's avatar
con committed
        QTextCursor cursor = textCursor();
        if (d->m_inBlockSelectionMode)
            cursor.clearSelection();
        const TabSettings &ts = d->m_document->tabSettings();
        cursor.beginEditBlock();

        int extraBlocks = paragraphSeparatorAboutToBeInserted(cursor); // virtual
        if (ts.m_autoIndent) {
con's avatar
con committed
            indent(document(), cursor, QChar::Null);
        } else {
            cursor.insertBlock();

            // After inserting the block, to avoid duplicating whitespace on the same line
            const QString previousBlockText = cursor.block().previous().text();
            cursor.insertText(ts.indentationString(previousBlockText));
con's avatar
con committed
        }
        cursor.endEditBlock();
con's avatar
con committed
        e->accept();

        if (extraBlocks > 0) {
            QTextCursor ensureVisible = cursor;
            while (extraBlocks > 0) {
                --extraBlocks;
                ensureVisible.movePosition(QTextCursor::NextBlock);
            }
            setTextCursor(ensureVisible);
        }

con's avatar
con committed
        setTextCursor(cursor);
        return;
    } else if (!ro
               && (e == QKeySequence::MoveToStartOfBlock
                   || e == QKeySequence::SelectStartOfBlock)){
        if ((e->modifiers() & (Qt::AltModifier | Qt::ShiftModifier)) == (Qt::AltModifier | Qt::ShiftModifier))
            d->m_lastEventWasBlockSelectionEvent = true;
        handleHomeKey(e == QKeySequence::SelectStartOfBlock);
        e->accept();
        return;
    } else if (!ro
               && (e == QKeySequence::MoveToStartOfLine
                   || e == QKeySequence::SelectStartOfLine)){
        if ((e->modifiers() & (Qt::AltModifier | Qt::ShiftModifier)) == (Qt::AltModifier | Qt::ShiftModifier))
            d->m_lastEventWasBlockSelectionEvent = true;
        QTextCursor cursor = textCursor();
        if (QTextLayout *layout = cursor.block().layout()) {
            if (layout->lineForTextPosition(cursor.position() - cursor.block().position()).lineNumber() == 0) {
                handleHomeKey(e == QKeySequence::SelectStartOfLine);
                e->accept();
                return;
            }
        }
    } else if (!ro
               && e == QKeySequence::DeleteStartOfWord
               && d->m_document->tabSettings().m_autoIndent
               && !textCursor().hasSelection()){
        e->accept();
        QTextCursor c = textCursor();
        int pos = c.position();
        c.movePosition(QTextCursor::PreviousWord);
        int targetpos = c.position();
        forever {
            handleBackspaceKey();
            int cpos = textCursor().position();
            if (cpos == pos || cpos <= targetpos)
                break;
            pos = cpos;
        }
        setTextCursor(textCursor()); // make cursor visible
con's avatar
con committed
    } else switch (e->key()) {


#if 0
    case Qt::Key_Dollar: {
            d->m_overlay->setVisible(!d->m_overlay->isVisible());
            d->m_overlay->setCursor(textCursor());
            e->accept();
con's avatar
con committed
        return;

    } break;
#endif
    case Qt::Key_Tab:
Joel Nordell's avatar
Joel Nordell committed
    case Qt::Key_Backtab: {
con's avatar
con committed
        if (ro) break;
        if (d->m_snippetOverlay->isVisible() && !d->m_snippetOverlay->isEmpty()) {
            d->snippetTabOrBacktab(e->key() == Qt::Key_Tab);
            e->accept();
            return;
        }
Joel Nordell's avatar
Joel Nordell committed
        QTextCursor cursor = textCursor();
        int newPosition;
        if (d->m_document->tabSettings().tabShouldIndent(document(), cursor, &newPosition)) {
            if (newPosition != cursor.position() && !cursor.hasSelection()) {
                cursor.setPosition(newPosition);
                setTextCursor(cursor);
            }
            indent(document(), cursor, QChar::Null);
        } else {
            indentOrUnindent(e->key() == Qt::Key_Tab);
        }
con's avatar
con committed
        e->accept();
        return;
Joel Nordell's avatar
Joel Nordell committed
    } break;
con's avatar
con committed
    case Qt::Key_Backspace:
        if (ro) break;
        if ((e->modifiers() & (Qt::ControlModifier
con's avatar
con committed
                               | Qt::ShiftModifier
                               | Qt::AltModifier
                               | Qt::MetaModifier)) == Qt::NoModifier
            && !textCursor().hasSelection()) {
            handleBackspaceKey();
            setTextCursor(textCursor()); // make cursor visible
con's avatar
con committed
            e->accept();
            return;
        }
        break;
    case Qt::Key_Up:
    case Qt::Key_Down:
        if (e->modifiers() & Qt::ControlModifier) {
            verticalScrollBar()->triggerAction(
                    e->key() == Qt::Key_Up ? QAbstractSlider::SliderSingleStepSub :
                                             QAbstractSlider::SliderSingleStepAdd);
            e->accept();
            return;
        }
        // fall through
    case Qt::Key_End:
con's avatar
con committed
    case Qt::Key_Right:
    case Qt::Key_Left:
Oswald Buddenhagen's avatar
Oswald Buddenhagen committed
#ifndef Q_WS_MAC
con's avatar
con committed
        if ((e->modifiers() & (Qt::AltModifier | Qt::ShiftModifier)) == (Qt::AltModifier | Qt::ShiftModifier)) {

            d->m_lastEventWasBlockSelectionEvent = true;

            if (d->m_inBlockSelectionMode) {
                if (e->key() == Qt::Key_Right && textCursor().atBlockEnd()) {
                    d->m_blockSelectionExtraX++;
                    viewport()->update();
                    e->accept();
                    return;
                } else if (e->key() == Qt::Key_Left && d->m_blockSelectionExtraX > 0) {
                    d->m_blockSelectionExtraX--;
                    e->accept();
                    viewport()->update();
                    return;
                }
            }

            e = new QKeyEvent(
                e->type(),
                e->key(),
                e->modifiers() & ~Qt::AltModifier,
                e->text(),
                e->isAutoRepeat(),
                e->count()
                );
        }
#endif
        break;
    case Qt::Key_PageUp:
    case Qt::Key_PageDown:
        if (e->modifiers() == Qt::ControlModifier) {
            verticalScrollBar()->triggerAction(
                    e->key() == Qt::Key_PageUp ? QAbstractSlider::SliderPageStepSub :
                                                 QAbstractSlider::SliderPageStepAdd);
            e->accept();
            return;
        }
        break;

    default:
        break;
    }

    if (d->m_inBlockSelectionMode) {
        QString text = e->text();
        if (!text.isEmpty() && (text.at(0).isPrint() || text.at(0) == QLatin1Char('\t'))) {
            d->removeBlockSelection(text);
            goto skip_event;
        }
    }

mae's avatar
mae committed
    if (e->key() == Qt::Key_H && e->modifiers() ==
#ifdef Q_OS_DARWIN
        Qt::MetaModifier
#else
        Qt::ControlModifier
#endif
        ) {
        universalHelper();
        e->accept();
        return;
    }

    if (d->m_snippetOverlay->isVisible()
        && (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace)) {
        d->snippetCheckCursor(textCursor());
    if (ro || e->text().isEmpty() || !e->text().at(0).isPrint()) {
        QPlainTextEdit::keyPressEvent(e);
    } else if ((e->modifiers() & (Qt::ControlModifier|Qt::AltModifier)) != Qt::ControlModifier){
        QTextCursor cursor = textCursor();
        QString text = e->text();
        QString autoText = autoComplete(cursor, text);
        QChar electricChar;
        if (d->m_document->tabSettings().m_autoIndent) {
                if (isElectricCharacter(c)) {
                    electricChar = c;
                    break;
                }
            }
        }
        if (d->m_snippetOverlay->isVisible())
            d->snippetCheckCursor(cursor);

        bool doEditBlock = !(electricChar.isNull() && autoText.isEmpty());
        if (doEditBlock)
            cursor.beginEditBlock();
        cursor.insertText(text);
        if (!autoText.isEmpty()) {
            int pos = cursor.position();
            cursor.insertText(autoText);
            cursor.setPosition(pos);
        }
            indent(document(), cursor, electricChar);
            cursor.endEditBlock();
        setTextCursor(cursor);
    }
con's avatar
con committed

skip_event:
    if (!ro && e->key() == Qt::Key_Delete && d->m_parenthesesMatchingEnabled)
        d->m_parenthesesMatchingTimer->start(50);

con's avatar
con committed

    if (!ro && d->m_contentsChanged && !e->text().isEmpty() && e->text().at(0).isPrint())
        emit requestAutoCompletion(editableInterface(), false);

    if (e != original_e)
        delete e;
}

mae's avatar
mae committed
void BaseTextEditor::insertCodeSnippet(const QTextCursor &cursor_arg, const QString &snippet)
    if ((snippet.count('$') % 2) != 0) {
        qWarning() << "invalid snippet";
        return;
    }

mae's avatar
mae committed
    QList<QTextEdit::ExtraSelection> selections;

    QTextCursor cursor = cursor_arg;
    cursor.beginEditBlock();
    cursor.removeSelectedText();
    const int startCursorPosition = cursor.position();

mae's avatar
mae committed
    QMap<int, int> positions;

    while (pos < snippet.size()) {
        if (snippet.at(pos) != QChar::ObjectReplacementCharacter) {
            const int start = pos;
            do { ++pos; }
            while (pos < snippet.size() && snippet.at(pos) != QChar::ObjectReplacementCharacter);
            cursor.insertText(snippet.mid(start, pos - start));
        } else {
            // the start of a place holder.
            const int start = ++pos;
            for (; pos < snippet.size(); ++pos) {
                if (snippet.at(pos) == QChar::ObjectReplacementCharacter)
                    break;
            }

            Q_ASSERT(pos < snippet.size());
            Q_ASSERT(snippet.at(pos) == QChar::ObjectReplacementCharacter);

            const QString textToInsert = snippet.mid(start, pos - start);

            int cursorPosition = cursor.position();
            cursor.insertText(textToInsert);

mae's avatar
mae committed
            if (textToInsert.isEmpty()) {
                positions.insert(cursorPosition, 0);
            } else {
                positions.insert(cursorPosition-1, textToInsert.length()+1);
            }
mae's avatar
mae committed
    QMapIterator<int,int> it(positions);
    while (it.hasNext()) {
        it.next();
        int length = it.value();
        int position = it.key();

        QTextCursor tc(document());
        tc.setPosition(position);
        tc.setPosition(position + length, QTextCursor::KeepAnchor);
        QTextEdit::ExtraSelection selection;
        selection.cursor = tc;
mae's avatar
mae committed
        selection.format = (length ? d->m_occurrencesFormat : d->m_occurrenceRenameFormat);
mae's avatar
mae committed
        selections.append(selection);
    }

    cursor.setPosition(startCursorPosition, QTextCursor::KeepAnchor);
    indent(cursor.document(), cursor, QChar());
mae's avatar
mae committed
    cursor.endEditBlock();
    setExtraSelections(BaseTextEditor::SnippetPlaceholderSelection, selections);

    if (! selections.isEmpty()) {
        const QTextEdit::ExtraSelection &selection = selections.first();

        cursor = textCursor();
        if (selection.cursor.hasSelection()) {
            cursor.setPosition(selection.cursor.selectionStart()+1);
            cursor.setPosition(selection.cursor.selectionEnd(), QTextCursor::KeepAnchor);
        } else {
            cursor.setPosition(selection.cursor.position());
        }
mae's avatar
mae committed

void BaseTextEditor::universalHelper()
{
    // Test function for development. Place your new fangled experiment here to
    // give it proper scrutiny before pushing it onto others.
void BaseTextEditor::setTextCursor(const QTextCursor &cursor)
{
    // workaround for QTextControl bug
    bool selectionChange = cursor.hasSelection() || textCursor().hasSelection();
    QTextCursor c = cursor;
    c.setVisualNavigation(true);
    QPlainTextEdit::setTextCursor(c);
    if (selectionChange)
        slotSelectionChanged();
}

void BaseTextEditor::gotoLine(int line, int column)
con's avatar
con committed
{
    d->m_lastCursorChangeWasInteresting = false; // avoid adding the previous position to history
con's avatar
con committed
    const int blockNumber = line - 1;
    const QTextBlock &block = document()->findBlockByNumber(blockNumber);
    if (block.isValid()) {
        QTextCursor cursor(block);
        if (column > 0) {
            cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, column);
        } else {
            int pos = cursor.position();
            while (characterAt(pos).category() == QChar::Separator_Space) {
                ++pos;
            }
            cursor.setPosition(pos);
        }
        setTextCursor(cursor);
        centerCursor();
    }
    saveCurrentCursorPositionForNavigation();
con's avatar
con committed
}

int BaseTextEditor::position(ITextEditor::PositionOperation posOp, int at) const
{
    QTextCursor tc = textCursor();

    if (at != -1)
        tc.setPosition(at);

    if (posOp == ITextEditor::Current)
        return tc.position();

    switch (posOp) {
    case ITextEditor::EndOfLine:
        tc.movePosition(QTextCursor::EndOfLine);
        return tc.position();
    case ITextEditor::StartOfLine:
        tc.movePosition(QTextCursor::StartOfLine);
        return tc.position();
    case ITextEditor::Anchor:
        if (tc.hasSelection())
            return tc.anchor();
        break;
    case ITextEditor::EndOfDoc:
        tc.movePosition(QTextCursor::End);
        return tc.position();
    default:
        break;
    }

    return -1;
}

void BaseTextEditor::convertPosition(int pos, int *line, int *column) const
{
    QTextBlock block = document()->findBlock(pos);
    if (!block.isValid()) {
        (*line) = -1;
        (*column) = -1;
    } else {
        (*line) = block.blockNumber() + 1;
        (*column) = pos - block.position();
    }
}

QChar BaseTextEditor::characterAt(int pos) const
{
    return document()->characterAt(pos);
}

bool BaseTextEditor::event(QEvent *e)
{
    d->m_contentsChanged = false;
con's avatar
con committed
    switch (e->type()) {
    case QEvent::ShortcutOverride:
        if (static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape && d->m_snippetOverlay->isVisible()) {
            e->accept();
            return true;
        }
        e->ignore(); // we are a really nice citizen
con's avatar
con committed
        return true;
con's avatar
con committed
    default:
        break;
    }

    return QPlainTextEdit::event(e);
}

void BaseTextEditor::duplicateFrom(BaseTextEditor *editor)
{
    if (this == editor)
        return;
    setDisplayName(editor->displayName());
    d->m_revisionsVisible = editor->d->m_revisionsVisible;
    if (d->m_document == editor->d->m_document)
        return;
    d->setupDocumentSignals(editor->d->m_document);
    d->m_document = editor->d->m_document;
}

QString BaseTextEditor::displayName() const
{
    return d->m_displayName;
}

void BaseTextEditor::setDisplayName(const QString &title)
{
    d->m_displayName = title;
}

BaseTextDocument *BaseTextEditor::baseTextDocument() const
{
    return d->m_document;
}

void BaseTextEditor::setBaseTextDocument(BaseTextDocument *doc)
{
    if (doc) {
        d->setupDocumentSignals(doc);
        d->m_document = doc;
    }
}

// called before reload
con's avatar
con committed
void BaseTextEditor::memorizeCursorPosition()
{
    d->m_tempState = saveState();
}

// called after reload
con's avatar
con committed
void BaseTextEditor::restoreCursorPosition()
{
    restoreState(d->m_tempState);
}

QByteArray BaseTextEditor::saveState() const
{
    QByteArray state;
    QDataStream stream(&state, QIODevice::WriteOnly);
    stream << 1; // version number
con's avatar
con committed
    stream << verticalScrollBar()->value();
    stream << horizontalScrollBar()->value();
    int line, column;
    convertPosition(textCursor().position(), &line, &column);
    stream << line;
    stream << column;

    // store code folding state
    QList<int> collapsedBlocks;
    QTextBlock block = document()->firstBlock();
    while (block.isValid()) {
        if (block.userData() && static_cast<TextBlockUserData*>(block.userData())->collapsed()) {
            int number = block.blockNumber();
            if (static_cast<TextBlockUserData*>(block.userData())->collapseMode()
                == TextBlockUserData::CollapseThis)
                number--;
            collapsedBlocks += number;
        }
        block = block.next();
    }
    stream << collapsedBlocks;

con's avatar
con committed
    return state;
}

bool BaseTextEditor::restoreState(const QByteArray &state)
{
    if (state.isEmpty()) {
        if (d->m_displaySettings.m_autoFoldFirstComment)
            d->collapseLicenseHeader();
        return false;
    }
con's avatar
con committed
    int version;
    int vval;
    int hval;
    int lval;
    int cval;
    QDataStream stream(state);
    stream >> version;
    stream >> vval;
    stream >> hval;
    stream >> lval;
    stream >> cval;

    if (version >= 1) {
        QList<int> collapsedBlocks;
        stream >> collapsedBlocks;
        QTextDocument *doc = document();
        foreach(int blockNumber, collapsedBlocks) {
            QTextBlock block = doc->findBlockByNumber(qMax(0, blockNumber));
            if (block.isValid())
                TextBlockUserData::doCollapse(block, false);
        }
    } else {
        if (d->m_displaySettings.m_autoFoldFirstComment)
            d->collapseLicenseHeader();
    d->m_lastCursorChangeWasInteresting = false; // avoid adding last position to history
    gotoLine(lval, cval);
con's avatar
con committed
    verticalScrollBar()->setValue(vval);
    horizontalScrollBar()->setValue(hval);
    saveCurrentCursorPositionForNavigation();
con's avatar
con committed
    return true;
}

void BaseTextEditor::setDefaultPath(const QString &defaultPath)
{
    baseTextDocument()->setDefaultPath(defaultPath);
}

void BaseTextEditor::setSuggestedFileName(const QString &suggestedFileName)
{
    baseTextDocument()->setSuggestedFileName(suggestedFileName);
}

void BaseTextEditor::setParenthesesMatchingEnabled(bool b)
{
    d->m_parenthesesMatchingEnabled = b;
}

bool BaseTextEditor::isParenthesesMatchingEnabled() const
{
    return d->m_parenthesesMatchingEnabled;
}

void BaseTextEditor::setHighlightCurrentLine(bool b)
{
    d->m_highlightCurrentLine = b;
con's avatar
con committed
}

bool BaseTextEditor::highlightCurrentLine() const
{
    return d->m_highlightCurrentLine;
}

void BaseTextEditor::setLineNumbersVisible(bool b)
{
    d->m_lineNumbersVisible = b;
    slotUpdateExtraAreaWidth();
}

bool BaseTextEditor::lineNumbersVisible() const
{
    return d->m_lineNumbersVisible;
}

void BaseTextEditor::setMarksVisible(bool b)
{
    d->m_marksVisible = b;
    slotUpdateExtraAreaWidth();
}

bool BaseTextEditor::marksVisible() const
{
    return d->m_marksVisible;
}

void BaseTextEditor::setRequestMarkEnabled(bool b)
{
    d->m_requestMarkEnabled = b;
}

bool BaseTextEditor::requestMarkEnabled() const
{
    return d->m_requestMarkEnabled;
}

void BaseTextEditor::setLineSeparatorsAllowed(bool b)
{
    d->m_lineSeparatorsAllowed = b;
}

bool BaseTextEditor::lineSeparatorsAllowed() const
{
    return d->m_lineSeparatorsAllowed;
}

void BaseTextEditor::setCodeFoldingVisible(bool b)
{
    d->m_codeFoldingVisible = b && d->m_codeFoldingSupported;
con's avatar
con committed
    slotUpdateExtraAreaWidth();
}

bool BaseTextEditor::codeFoldingVisible() const
{
    return d->m_codeFoldingVisible;
}

/**
 * Sets whether code folding is supported by the syntax highlighter. When not
 * supported (the default), this makes sure the code folding is not shown.
 *
 * Needs to be called before calling setCodeFoldingVisible.
 */
void BaseTextEditor::setCodeFoldingSupported(bool b)
{
    d->m_codeFoldingSupported = b;
}

bool BaseTextEditor::codeFoldingSupported() const
{
    return d->m_codeFoldingSupported;
}

void BaseTextEditor::setMouseNavigationEnabled(bool b)
{
    d->m_behaviorSettings.m_mouseNavigation = b;
}

bool BaseTextEditor::mouseNavigationEnabled() const
{
    return d->m_behaviorSettings.m_mouseNavigation;
}

void BaseTextEditor::setScrollWheelZoomingEnabled(bool b)
{
    d->m_behaviorSettings.m_scrollWheelZooming = b;
}

bool BaseTextEditor::scrollWheelZoomingEnabled() const
{
    return d->m_behaviorSettings.m_scrollWheelZooming;
con's avatar
con committed
void BaseTextEditor::setRevisionsVisible(bool b)
{
    d->m_revisionsVisible = b;
    slotUpdateExtraAreaWidth();
}

bool BaseTextEditor::revisionsVisible() const
{
    return d->m_revisionsVisible;
}

void BaseTextEditor::setVisibleWrapColumn(int column)
{
    d->m_visibleWrapColumn = column;
    viewport()->update();
}

int BaseTextEditor::visibleWrapColumn() const
{
    return d->m_visibleWrapColumn;
}

//--------- BaseTextEditorPrivate -----------

BaseTextEditorPrivate::BaseTextEditorPrivate()
    :
    m_contentsChanged(false),
    m_lastCursorChangeWasInteresting(false),
con's avatar
con committed
    m_document(new BaseTextDocument()),
    m_parenthesesMatchingEnabled(false),
    m_extraArea(0),
    m_mouseOnCollapsedMarker(false),
con's avatar
con committed
    m_marksVisible(false),
    m_codeFoldingVisible(false),
    m_codeFoldingSupported(false),
con's avatar
con committed
    m_revisionsVisible(false),
    m_lineNumbersVisible(true),
    m_highlightCurrentLine(true),
    m_requestMarkEnabled(true),
    m_lineSeparatorsAllowed(false),
    m_visibleWrapColumn(0),
con's avatar
con committed
    m_editable(0),
    m_actionHack(0),
    m_inBlockSelectionMode(false),
    m_lastEventWasBlockSelectionEvent(false),
    m_moveLineUndoHack(false),
    m_cursorBlockNumber(-1)
con's avatar
con committed
{
}

BaseTextEditorPrivate::~BaseTextEditorPrivate()
{
}

void BaseTextEditorPrivate::setupDocumentSignals(BaseTextDocument *document)
{
    BaseTextDocument *oldDocument = q->baseTextDocument();
    if (oldDocument) {
        q->disconnect(oldDocument->document(), 0, q, 0);
        q->disconnect(oldDocument, 0, q, 0);
    }

    QTextDocument *doc = document->document();
    TextEditDocumentLayout *documentLayout = qobject_cast<TextEditDocumentLayout*>(doc->documentLayout());
    if (!documentLayout) {
        QTextOption opt = doc->defaultTextOption();
        opt.setTextDirection(Qt::LeftToRight);
        opt.setFlags(opt.flags() | QTextOption::IncludeTrailingSpaces
                | QTextOption::AddSpaceForLineAndParagraphSeparators
                );
        doc->setDefaultTextOption(opt);
        documentLayout = new TextEditDocumentLayout(doc);
        doc->setDocumentLayout(documentLayout);
    }


    q->setDocument(doc);
    QObject::connect(documentLayout, SIGNAL(updateBlock(QTextBlock)), q, SLOT(slotUpdateBlockNotify(QTextBlock)));
    QObject::connect(q, SIGNAL(requestBlockUpdate(QTextBlock)), documentLayout, SIGNAL(updateBlock(QTextBlock)));
    QObject::connect(doc, SIGNAL(modificationChanged(bool)), q, SIGNAL(changed()));
    QObject::connect(doc, SIGNAL(contentsChange(int,int,int)), q,
        SLOT(editorContentsChange(int,int,int)), Qt::DirectConnection);
    QObject::connect(document, SIGNAL(changed()), q, SIGNAL(changed()));
    QObject::connect(document, SIGNAL(titleChanged(QString)), q, SLOT(setDisplayName(const QString &)));
    QObject::connect(document, SIGNAL(aboutToReload()), q, SLOT(memorizeCursorPosition()));
    QObject::connect(document, SIGNAL(reloaded()), q, SLOT(restoreCursorPosition()));
    q->slotUpdateExtraAreaWidth();
}


void BaseTextEditorPrivate::snippetCheckCursor(const QTextCursor &cursor)
{
    if (!m_snippetOverlay->isVisible() || m_snippetOverlay->isEmpty())
        return;

    QTextCursor start = cursor;
    start.setPosition(cursor.selectionStart());
    QTextCursor end = cursor;
    end.setPosition(cursor.selectionEnd());
    if (!m_snippetOverlay->hasCursorInSelection(start)
        || !m_snippetOverlay->hasCursorInSelection(end)) {
        m_snippetOverlay->setVisible(false);
        m_snippetOverlay->clear();
    }
}

void BaseTextEditorPrivate::snippetTabOrBacktab(bool forward)
{
    if (!m_snippetOverlay->isVisible() || m_snippetOverlay->isEmpty())
        return;
    QTextCursor cursor = q->textCursor();
    OverlaySelection final;
    if (forward) {
        for (int i = 0; i < m_snippetOverlay->m_selections.count(); ++i){
            const OverlaySelection &selection = m_snippetOverlay->m_selections.at(i);
            if (selection.m_cursor_begin.position() >= cursor.position()
                && selection.m_cursor_end.position() > cursor.position()) {
                final = selection;
                break;
            }
        }
    } else {
        for (int i = m_snippetOverlay->m_selections.count()-1; i >= 0; --i){
            const OverlaySelection &selection = m_snippetOverlay->m_selections.at(i);
            if (selection.m_cursor_end.position() < cursor.position()) {
                final = selection;
                break;
            }
        }

    }
    if (final.m_cursor_begin.isNull())
        final = forward ? m_snippetOverlay->m_selections.first() : m_snippetOverlay->m_selections.last();

    if (final.m_cursor_begin.position() == final.m_cursor_end.position()) { // empty tab stop
        cursor.setPosition(final.m_cursor_end.position());
    } else {
        cursor.setPosition(final.m_cursor_begin.position()+1);
        cursor.setPosition(final.m_cursor_end.position(), QTextCursor::KeepAnchor);
    }
    q->setTextCursor(cursor);
}

con's avatar
con committed
bool Parenthesis::hasClosingCollapse(const Parentheses &parentheses)
{
    return closeCollapseAtPos(parentheses) >= 0;
}


int Parenthesis::closeCollapseAtPos(const Parentheses &parentheses)
{
    int depth = 0;
    for (int i = 0; i < parentheses.size(); ++i) {
        const Parenthesis &p = parentheses.at(i);
        if (p.chr == QLatin1Char('{')
            || p.chr == QLatin1Char('+')
            || p.chr == QLatin1Char('[')) {
con's avatar
con committed
            ++depth;
        } else if (p.chr == QLatin1Char('}')
            || p.chr == QLatin1Char('-')
            || p.chr == QLatin1Char(']')) {
con's avatar
con committed
            if (--depth < 0)
                return p.pos;
        }
    }
    return -1;
}

int Parenthesis::collapseAtPos(const Parentheses &parentheses, QChar *character)
{
    int result = -1;
    QChar c;

    int depth = 0;
    for (int i = 0; i < parentheses.size(); ++i) {
        const Parenthesis &p = parentheses.at(i);
        if (p.chr == QLatin1Char('{')
            || p.chr == QLatin1Char('+')
            || p.chr == QLatin1Char('[')) {
con's avatar
con committed
            if (depth == 0) {
                result = p.pos;
                c = p.chr;
            }
            ++depth;
        } else if (p.chr == QLatin1Char('}')
            || p.chr == QLatin1Char('-')
            || p.chr == QLatin1Char(']')) {
con's avatar
con committed
            if (--depth < 0)
                depth = 0;
            result = -1;
        }
    }
    if (result >= 0 && character)
        *character = c;
    return result;
}


int TextBlockUserData::collapseAtPos(QChar *character) const
con's avatar
con committed
{
    return Parenthesis::collapseAtPos(m_parentheses, character);
con's avatar
con committed
}

int TextBlockUserData::braceDepthDelta() const
{
    int delta = 0;
    for (int i = 0; i < m_parentheses.size(); ++i) {
        switch (m_parentheses.at(i).chr.unicode()) {
        case '{': case '+': case '[': ++delta; break;
        case '}': case '-': case ']': --delta; break;
        default: break;
        }
    }
    return delta;
}
con's avatar
con committed

void TextEditDocumentLayout::setParentheses(const QTextBlock &block, const Parentheses &parentheses)
{
    if (parentheses.isEmpty()) {
        if (TextBlockUserData *userData = testUserData(block))
            userData->clearParentheses();
    } else {
        userData(block)->setParentheses(parentheses);
    }
}

Parentheses TextEditDocumentLayout::parentheses(const QTextBlock &block)
{
    if (TextBlockUserData *userData = testUserData(block))
        return userData->parentheses();
    return Parentheses();
}

bool TextEditDocumentLayout::hasParentheses(const QTextBlock &block)
{
    if (TextBlockUserData *userData = testUserData(block))
        return userData->hasParentheses();
    return false;
}

int TextEditDocumentLayout::braceDepthDelta(const QTextBlock &block)
{
    if (TextBlockUserData *userData = testUserData(block))
        return userData->braceDepthDelta();
    return 0;
}

int TextEditDocumentLayout::braceDepth(const QTextBlock &block)
{
    int state = block.userState();
    if (state == -1)
        return 0;
    return state >> 8;
}

void TextEditDocumentLayout::setBraceDepth(QTextBlock &block, int depth)
{
    int state = block.userState();
    if (state == -1)
        state = 0;
    state = state & 0xff;
    block.setUserState((depth << 8) | state);
}

void TextEditDocumentLayout::changeBraceDepth(QTextBlock &block, int delta)
{
    if (delta)
        setBraceDepth(block, braceDepth(block) + delta);
}
con's avatar
con committed

bool TextEditDocumentLayout::setIfdefedOut(const QTextBlock &block)
{
    return userData(block)->setIfdefedOut();
}

bool TextEditDocumentLayout::clearIfdefedOut(const QTextBlock &block)
{
    if (TextBlockUserData *userData = testUserData(block))
        return userData->clearIfdefedOut();
    return false;
}

bool TextEditDocumentLayout::ifdefedOut(const QTextBlock &block)
{
    if (TextBlockUserData *userData = testUserData(block))
        return userData->ifdefedOut();
    return false;
}


TextEditDocumentLayout::TextEditDocumentLayout(QTextDocument *doc)
    :QPlainTextDocumentLayout(doc) {
    lastSaveRevision = 0;
    hasMarks = 0;
}

TextEditDocumentLayout::~TextEditDocumentLayout()
{
}

QRectF TextEditDocumentLayout::blockBoundingRect(const QTextBlock &block) const
{
    QRectF r = QPlainTextDocumentLayout::blockBoundingRect(block);
    return r;
}


bool BaseTextEditor::viewportEvent(QEvent *event)
{
    d->m_contentsChanged = false;
con's avatar
con committed
    if (event->type() == QEvent::ContextMenu) {
        const QContextMenuEvent *ce = static_cast<QContextMenuEvent*>(event);
        if (ce->reason() == QContextMenuEvent::Mouse && !textCursor().hasSelection())
            setTextCursor(cursorForPosition(ce->pos()));
    } else if (event->type() == QEvent::ToolTip) {
        const QHelpEvent *he = static_cast<QHelpEvent*>(event);
        if (QApplication::keyboardModifiers() & Qt::ControlModifier)
            return true; // eat tooltip event when control is pressed
con's avatar
con committed
        const QPoint &pos = he->pos();

        // Allow plugins to show tooltips
        const QTextCursor &c = cursorForPosition(pos);
        QPoint cursorPos = mapToGlobal(cursorRect(c).bottomRight() + QPoint(1,1));
        cursorPos.setX(cursorPos.x() + d->m_extraArea->width());

        editableInterface(); // create if necessary

        emit d->m_editable->tooltipRequested(editableInterface(), cursorPos, c.position());
        return true;
    }
    return QPlainTextEdit::viewportEvent(event);
}


void BaseTextEditor::resizeEvent(QResizeEvent *e)
{
    QPlainTextEdit::resizeEvent(e);
    QRect cr = rect();
con's avatar
con committed
    d->m_extraArea->setGeometry(
        QStyle::visualRect(layoutDirection(), cr,
                           QRect(cr.left(), cr.top(), extraAreaWidth(), cr.height())));
}

QRect BaseTextEditor::collapseBox()
con's avatar
con committed
{
    if (d->m_highlightBlocksInfo.isEmpty() || d->extraAreaHighlightCollapseBlockNumber < 0)
        return QRect();
con's avatar
con committed

    QTextBlock begin = document()->findBlockByNumber(d->m_highlightBlocksInfo.open.last());
    if (TextBlockUserData::hasCollapseAfter(begin.previous()))
        begin = begin.previous();
    QTextBlock end = document()->findBlockByNumber(d->m_highlightBlocksInfo.close.first());
    if (!begin.isValid() || !end.isValid())
        return QRect();
    QRectF br = blockBoundingGeometry(begin).translated(contentOffset());
    QRectF er = blockBoundingGeometry(end).translated(contentOffset());

    return QRect(d->m_extraArea->width() - collapseBoxWidth(fontMetrics()),
                 collapseBoxWidth(fontMetrics()),
                 er.bottom() - br.top());
con's avatar
con committed
}

QTextBlock BaseTextEditor::collapsedBlockAt(const QPoint &pos, QRect *box) const {
    QPointF offset(contentOffset());
    QTextBlock block = firstVisibleBlock();
    int top = (int)blockBoundingGeometry(block).translated(offset).top();
    int bottom = top + (int)blockBoundingRect(block).height();

    int viewportHeight = viewport()->height();

    while (block.isValid() && top <= viewportHeight) {
        QTextBlock nextBlock = block.next();
        if (block.isVisible() && bottom >= 0) {
            if (nextBlock.isValid() && !nextBlock.isVisible()) {
                QTextLayout *layout = block.layout();
                QTextLine line = layout->lineAt(layout->lineCount()-1);
                QRectF lineRect = line.naturalTextRect().translated(offset.x(), top);
                lineRect.adjust(0, 0, -1, -1);

                QRectF collapseRect(lineRect.right() + 12,
                                    lineRect.top(),
                                    fontMetrics().width(QLatin1String(" {...}; ")),
                                    lineRect.height());
                if (collapseRect.contains(pos)) {
                    QTextBlock result = block;
                    if (box)
                        *box = collapseRect.toAlignedRect();
                    return result;
                } else {
                    block = nextBlock;
                    while (nextBlock.isValid() && !nextBlock.isVisible()) {
                        block = nextBlock;
                        nextBlock = block.next();
                    }
                }
            }
        }

        block = nextBlock;
        top = bottom;
        bottom = top + (int)blockBoundingRect(block).height();
    }
    return QTextBlock();
}

void BaseTextEditorPrivate::highlightSearchResults(const QTextBlock &block,
                                                   TextEditorOverlay *overlay)
con's avatar
con committed
{
    if (m_searchExpr.isEmpty())
        return;

    int blockPosition = block.position();

    QTextCursor cursor = q->textCursor();
con's avatar
con committed
    QString text = block.text();
    text.replace(QChar::Nbsp, QLatin1Char(' '));
    int idx = -1;

    int m_findScopeFirstColumn = 0;
mae's avatar
mae committed
    if (!m_findScopeStart.isNull() && m_findScopeVerticalBlockSelection)  {
mae's avatar
mae committed
        m_findScopeFirstColumn = m_findScopeStart.positionInBlock() + 1;
con's avatar
con committed
    while (idx < text.length()) {
        idx = m_searchExpr.indexIn(text, idx + l);
con's avatar
con committed
        if (idx < 0)
            break;
        l = m_searchExpr.matchedLength();
        if ((m_findFlags & Find::IFindSupport::FindWholeWords)
con's avatar
con committed
            && ((idx && text.at(idx-1).isLetterOrNumber())
                || (idx + l < text.length() && text.at(idx + l).isLetterOrNumber())))
            continue;

mae's avatar
mae committed
        if (!m_findScopeStart.isNull()) {
            if (blockPosition + idx < m_findScopeStart.position()
                || blockPosition + idx + l > m_findScopeEnd.position())
                continue;
            if (m_findScopeVerticalBlockSelection) {
                if (idx < m_findScopeFirstColumn
                    || idx + l > m_findScopeFirstColumn + m_findScopeVerticalBlockSelection)
                    continue;
            }
        }

        overlay->addOverlaySelection(blockPosition + idx,
                                     blockPosition + idx + l,
                                     m_searchResultFormat.background().color().darker(120),
                                     QColor(),
                                     (idx == cursor.selectionStart() - blockPosition
                                      && idx + l == cursor.selectionEnd() - blockPosition)?
                                     TextEditorOverlay::DropShadow : 0);
con's avatar
con committed
    }
}


namespace TextEditor {
    namespace Internal {
        struct BlockSelectionData {
            int selectionIndex;
            int selectionStart;
            int selectionEnd;
            int firstColumn;
            int lastColumn;
        };
    }
}

void BaseTextEditorPrivate::clearBlockSelection()
{
    if (m_inBlockSelectionMode) {
        m_inBlockSelectionMode = false;
        QTextCursor cursor = q->textCursor();
        cursor.clearSelection();
        q->setTextCursor(cursor);
    }
}

QString BaseTextEditorPrivate::copyBlockSelection()
{
    QString text;

    QTextCursor cursor = q->textCursor();
    if (!cursor.hasSelection())
        return text;

    QTextDocument *doc = q->document();
    int start = cursor.selectionStart();
    int end = cursor.selectionEnd();
    QTextBlock startBlock = doc->findBlock(start);
    int columnA = start - startBlock.position();
    QTextBlock endBlock = doc->findBlock(end);
    int columnB = end - endBlock.position();
    int firstColumn = qMin(columnA, columnB);
    int lastColumn = qMax(columnA, columnB) + m_blockSelectionExtraX;

    QTextBlock block = startBlock;
    for (;;) {

        cursor.setPosition(block.position() + qMin(block.length()-1, firstColumn));
        cursor.setPosition(block.position() + qMin(block.length()-1, lastColumn), QTextCursor::KeepAnchor);
        text += cursor.selectedText();
        if (block == endBlock)
            break;
        text += QLatin1Char('\n');
        block = block.next();
    }

    return text;
}

void BaseTextEditorPrivate::removeBlockSelection(const QString &text)
{
    QTextCursor cursor = q->textCursor();
    if (!cursor.hasSelection())
        return;

    QTextDocument *doc = q->document();
    int start = cursor.selectionStart();
    int end = cursor.selectionEnd();
    QTextBlock startBlock = doc->findBlock(start);
    int columnA = start - startBlock.position();
    QTextBlock endBlock = doc->findBlock(end);
    int columnB = end - endBlock.position();
    int firstColumn = qMin(columnA, columnB);
    int lastColumn = qMax(columnA, columnB) + m_blockSelectionExtraX;

    cursor.clearSelection();
    cursor.beginEditBlock();

    QTextBlock block = startBlock;
    for (;;) {

        cursor.setPosition(block.position() + qMin(block.length()-1, firstColumn));
        cursor.setPosition(block.position() + qMin(block.length()-1, lastColumn), QTextCursor::KeepAnchor);
        cursor.removeSelectedText();
        if (block == endBlock)
            break;
        block = block.next();
    }

    cursor.setPosition(start);
    if (!text.isEmpty())
        cursor.insertText(text);
    cursor.endEditBlock();
    q->setTextCursor(cursor);
}

void BaseTextEditorPrivate::moveCursorVisible(bool ensureVisible)
{
    QTextCursor cursor = q->textCursor();
    if (!cursor.block().isVisible()) {
        cursor.setVisualNavigation(true);
        cursor.movePosition(QTextCursor::Up);
        q->setTextCursor(cursor);
    }
    if (ensureVisible)
        q->ensureCursorVisible();
}

static QColor blendColors(const QColor &a, const QColor &b, int alpha)
{
    return QColor((a.red()   * (256 - alpha) + b.red()   * alpha) / 256,
                  (a.green() * (256 - alpha) + b.green() * alpha) / 256,
                  (a.blue()  * (256 - alpha) + b.blue()  * alpha) / 256);
}

mae's avatar
mae committed
static QColor calcBlendColor(const QColor &baseColor, int level, int count)
{
    QColor color80;
    QColor color90;

    if (baseColor.value() > 128) {
        const int f90 = 15;
        const int f80 = 30;
        color80.setRgb(qMax(0, baseColor.red() - f80),
                       qMax(0, baseColor.green() - f80),
                       qMax(0, baseColor.blue() - f80));
        color90.setRgb(qMax(0, baseColor.red() - f90),
                       qMax(0, baseColor.green() - f90),
                       qMax(0, baseColor.blue() - f90));
mae's avatar
mae committed
    } else {
        const int f90 = 20;
        const int f80 = 40;
        color80.setRgb(qMin(255, baseColor.red() + f80),
                       qMin(255, baseColor.green() + f80),
                       qMin(255, baseColor.blue() + f80));
        color90.setRgb(qMin(255, baseColor.red() + f90),
                       qMin(255, baseColor.green() + f90),
                       qMin(255, baseColor.blue() + f90));
mae's avatar
mae committed
    }

    if (level == count)
        return baseColor;
    if (level == 0)
        return color80;
    if (level == count - 1)
        return color90;

    const int blendFactor = level * (256 / (count - 2));

    return blendColors(color80, color90, blendFactor);
mae's avatar
mae committed
}

con's avatar
con committed
void BaseTextEditor::paintEvent(QPaintEvent *e)
{
    /*
      Here comes an almost verbatim copy of
      QPlainTextEdit::paintEvent() so we can adjust the extra
      selections dynamically to indicate all search results.
    */
    //begin QPlainTextEdit::paintEvent()

    QPainter painter(viewport());
    QTextDocument *doc = document();
    TextEditDocumentLayout *documentLayout = qobject_cast<TextEditDocumentLayout*>(doc->documentLayout());
hjk's avatar
hjk committed
    QTC_ASSERT(documentLayout, return);
con's avatar
con committed

    QPointF offset(contentOffset());
    QTextBlock textCursorBlock = textCursor().block();
con's avatar
con committed

    bool hasMainSelection = textCursor().hasSelection();
    bool suppressSyntaxInIfdefedOutBlock = (d->m_ifdefedOutFormat.foreground()
                                           != palette().foreground());
con's avatar
con committed
    QRect er = e->rect();
    QRect viewportRect = viewport()->rect();

    const QColor baseColor = palette().base().color();
    if (d->m_visibleWrapColumn > 0) {
        lineX = fontMetrics().averageCharWidth() * d->m_visibleWrapColumn + offset.x() + 4;

        if (lineX < viewportRect.width()) {
            const QBrush background = d->m_ifdefedOutFormat.background();
            painter.fillRect(QRectF(lineX, er.top(), viewportRect.width() - lineX, er.height()),

            const QColor col = (palette().base().color().value() > 128) ? Qt::black : Qt::white;
            const QPen pen = painter.pen();
            painter.setPen(blendColors(background.color(), col, 32));
            painter.drawLine(QPointF(lineX, er.top()), QPointF(lineX, er.bottom()));
            painter.setPen(pen);
        }
    // Set a brush origin so that the WaveUnderline knows where the wave started
    painter.setBrushOrigin(offset);

//    // keep right margin clean from full-width selection
//    int maxX = offset.x() + qMax((qreal)viewportRect.width(), documentLayout->documentSize().width())
//               - doc->documentMargin();
//    er.setRight(qMin(er.right(), maxX));
//    painter.setClipRect(er);
con's avatar
con committed

    bool editable = !isReadOnly();
    QTextBlock block = firstVisibleBlock();

    QAbstractTextDocumentLayout::PaintContext context = getPaintContext();

    if (!d->m_highlightBlocksInfo.isEmpty()) {
        // extra pass for the block highlight

        const int margin = 5;
        QTextBlock blockFP = block;
        QPointF offsetFP = offset;
        while (blockFP.isValid()) {
            QRectF r = blockBoundingRect(blockFP).translated(offsetFP);

            int n = blockFP.blockNumber();
            int depth = 0;
            foreach (int i, d->m_highlightBlocksInfo.open)
                if (n >= i)
                    ++depth;
            foreach (int i, d->m_highlightBlocksInfo.close)
                if (n > i)
                    --depth;

            int count = d->m_highlightBlocksInfo.count();
            if (count) {
                QRectF rr = r;
                rr.setWidth(viewport()->width());
                if (lineX > 0)
                    rr.setRight(qMin(lineX, rr.right()));
                for (int i = 0; i <= depth; ++i) {
                    int vi = i > 0 ? d->m_highlightBlocksInfo.visualIndent.at(i-1) : 0;
                    painter.fillRect(rr.adjusted(vi, 0, -8*i, 0), calcBlendColor(baseColor, i, count));
                    if (d->m_highlightCurrentLine && blockFP == textCursorBlock) {
                        QRectF rrr = blockFP.layout()->lineForTextPosition(textCursor().positionInBlock()).rect();
                        rrr.moveTop(rrr.top() + rr.top());
                        rrr.setLeft(rr.left());
                        rrr.setRight(rr.right());
                        painter.fillRect(rrr.adjusted(vi, 0, -8*i, 0),
                                         calcBlendColor(d->m_currentLineFormat.background().color(), i, count));
                    }
            }
            offsetFP.ry() += r.height();

            if (offsetFP.y() > viewportRect.height() + margin)
                break;

            blockFP = blockFP.next();
            if (!blockFP.isVisible()) {
                // invisible blocks do have zero line count
                blockFP = doc->findBlockByLineNumber(blockFP.firstLineNumber());
            }
        }
    }



mae's avatar
mae committed
    if (!d->m_findScopeStart.isNull()) {

        TextEditorOverlay *overlay = new TextEditorOverlay(this);
mae's avatar
mae committed
        overlay->addOverlaySelection(d->m_findScopeStart.position(),
                                     d->m_findScopeVerticalBlockSelection ?
                                     d->m_findScopeEnd.block().position()
                                     + d->m_findScopeVerticalBlockSelection
                                     + d->m_findScopeStart.positionInBlock() + 1
                                         : d->m_findScopeEnd.position(),
                                     d->m_searchScopeFormat.background().color().darker(120),
                                     d->m_searchScopeFormat.background().color(),
mae's avatar
mae committed
                                     TextEditorOverlay::ExpandBegin,
                                     d->m_findScopeVerticalBlockSelection);
        overlay->setAlpha(false);
        overlay->paint(&painter, e->rect());
        delete overlay;
con's avatar
con committed
    }

    BlockSelectionData *blockSelection = 0;

    if (d->m_inBlockSelectionMode
        && context.selections.count() && context.selections.last().cursor == textCursor()) {
        blockSelection = new BlockSelectionData;
        blockSelection->selectionIndex = context.selections.size()-1;
        const QAbstractTextDocumentLayout::Selection &selection = context.selections[blockSelection->selectionIndex];
        int start = blockSelection->selectionStart = selection.cursor.selectionStart();
        int end = blockSelection->selectionEnd = selection.cursor.selectionEnd();
        QTextBlock block = doc->findBlock(start);
        int columnA = start - block.position();
        block = doc->findBlock(end);
        int columnB = end - block.position();
        blockSelection->firstColumn = qMin(columnA, columnB);
        blockSelection->lastColumn = qMax(columnA, columnB) + d->m_blockSelectionExtraX;
    }

    QTextBlock visibleCollapsedBlock;
    QPointF visibleCollapsedBlockOffset;

    QTextLayout *cursor_layout = 0;
    QPointF cursor_offset;
    int cursor_cpos = 0;
    QPen cursor_pen;
    d->m_searchResultOverlay->clear();
    if (!d->m_searchExpr.isEmpty()) { // first pass for the search result overlays
        const int margin = 5;
        QTextBlock blockFP = block;
        QPointF offsetFP = offset;
        while (blockFP.isValid()) {
            QRectF r = blockBoundingRect(blockFP).translated(offsetFP);
con's avatar
con committed

            if (r.bottom() >= er.top() - margin && r.top() <= er.bottom() + margin) {
                d->highlightSearchResults(blockFP,
                                          d->m_searchResultOverlay);
            }
            offsetFP.ry() += r.height();

            if (offsetFP.y() > viewportRect.height() + margin)
                break;
            blockFP = blockFP.next();
            if (!blockFP.isVisible()) {
                // invisible blocks do have zero line count
                blockFP = doc->findBlockByLineNumber(blockFP.firstLineNumber());
            }
    } // end first pass

    d->m_searchResultOverlay->fill(&painter,
                                   d->m_searchResultFormat.background().color(),
                                   e->rect());
    while (block.isValid()) {

        QRectF r = blockBoundingRect(block).translated(offset);

        if (r.bottom() >= er.top() && r.top() <= er.bottom()) {
            if (TextEditDocumentLayout::ifdefedOut(block)) {
                rr.setRight(viewportRect.width() - offset.x());
                if (lineX > 0)
                    rr.setRight(qMin(lineX, rr.right()));
                painter.fillRect(rr, d->m_ifdefedOutFormat.background());
            }

            QTextLayout *layout = block.layout();
con's avatar
con committed

            QTextOption option = layout->textOption();
            if (suppressSyntaxInIfdefedOutBlock && TextEditDocumentLayout::ifdefedOut(block)) {
                option.setFlags(option.flags() | QTextOption::SuppressColors);
                painter.setPen(d->m_ifdefedOutFormat.foreground().color());
            } else {
                option.setFlags(option.flags() & ~QTextOption::SuppressColors);
                painter.setPen(context.palette.text().color());
            }
            layout->setTextOption(option);
            layout->setFont(doc->defaultFont()); // this really should be in qplaintextedit when creating the layout!
con's avatar
con committed

            int blpos = block.position();
            int bllen = block.length();

            QVector<QTextLayout::FormatRange> selections;
            QVector<QTextLayout::FormatRange> prioritySelections;
con's avatar
con committed

            for (int i = 0; i < context.selections.size(); ++i) {
                const QAbstractTextDocumentLayout::Selection &range = context.selections.at(i);
                const int selStart = range.cursor.selectionStart() - blpos;
                const int selEnd = range.cursor.selectionEnd() - blpos;
                if (selStart < bllen && selEnd > 0
                    && selEnd > selStart) {
                    QTextLayout::FormatRange o;
                    o.start = selStart;
                    o.length = selEnd - selStart;
                    o.format = range.format;
                    if (blockSelection && blockSelection->selectionIndex == i) {
                        o.start = qMin(blockSelection->firstColumn, bllen-1);
                        o.length = qMin(blockSelection->lastColumn, bllen-1) - o.start;
                    }
                    if ((hasMainSelection && i == context.selections.size()-1)
                        || (o.format.foreground().style() == Qt::NoBrush
                        && o.format.underlineStyle() != QTextCharFormat::NoUnderline
                        && o.format.background() == Qt::NoBrush))
                        prioritySelections.append(o);
con's avatar
con committed
                    else
                        selections.append(o);
                }
#if 0
                // we disable fullwidth selection. It's only used for m_highlightCurrentLine which we
                // do differently now
                else if (!range.cursor.hasSelection() && range.format.hasProperty(QTextFormat::FullWidthSelection)
                    && block.contains(range.cursor.position())) {
con's avatar
con committed
                    // for full width selections we don't require an actual selection, just
                    // a position to specify the line. that's more convenience in usage.
                    QTextLayout::FormatRange o;
                    QTextLine l = layout->lineForTextPosition(range.cursor.position() - blpos);
                    o.start = l.textStart();
                    o.length = l.textLength();
                    if (o.start + o.length == bllen - 1)
                        ++o.length; // include newline
                    o.format = range.format;
con's avatar
con committed
                }
con's avatar
con committed
            }
            selections += prioritySelections;
con's avatar
con committed

            if (d->m_highlightCurrentLine && block == textCursorBlock) {

                QRectF rr = layout->lineForTextPosition(textCursor().positionInBlock()).rect();
                rr.moveTop(rr.top() + r.top());
                rr.setLeft(0);
                rr.setRight(viewportRect.width() - offset.x());
                if (lineX > 0) {
                    if (lineX < rr.right()) {
                        QRectF rrr = rr;
                        rrr.setLeft(lineX);
                        painter.fillRect(rrr,
                                         blendColors(
                                                 d->m_currentLineFormat.background().color(),
                                                 d->m_ifdefedOutFormat.background().color(),
                                                 50)
                                         );
                        const QColor col = (palette().base().color().value() > 128) ? Qt::black : Qt::white;
                        const QPen pen = painter.pen();
                        painter.setPen(blendColors(d->m_currentLineFormat.background().color(), col, 32));
                        painter.drawLine(QPointF(lineX, rr.top()), QPointF(lineX, rr.bottom()));
                        painter.setPen(pen);
                    }
                    rr.setRight(qMin(lineX, rr.right()));
                }
                if (d->m_highlightBlocksInfo.isEmpty() || TextEditDocumentLayout::ifdefedOut(block))
                    painter.fillRect(rr, d->m_currentLineFormat.background());
            }

            bool drawCursor = ((editable || true) // we want the cursor in read-only mode
                               && context.cursorPosition >= blpos
                               && context.cursorPosition < blpos + bllen);

            bool drawCursorAsBlock = drawCursor && overwriteMode() ;

            if (drawCursorAsBlock) {
                int relativePos = context.cursorPosition - blpos;
                QTextLine line = layout->lineForTextPosition(relativePos);
                qreal x = line.cursorToX(relativePos);
                qreal w = 0;
                if (relativePos < line.textLength() - line.textStart()) {
                    w = line.cursorToX(relativePos + 1) - x;
                    if (doc->characterAt(context.cursorPosition) == QLatin1Char('\t')) {
                        int space = QFontMetrics(layout->font()).width(QLatin1Char(' '));
                        if (w > space) {
                            x += w-space;
                            w = space;
                        }
                    }
                } else
                    w = QFontMetrics(layout->font()).width(QLatin1Char(' ')); // in sync with QTextLine::draw()

                QRectF rr = line.rect();
                rr.moveTop(rr.top() + r.top());
                rr.moveLeft(r.left() + x);
                rr.setWidth(w);
                painter.fillRect(rr, palette().text());
                if (doSelection) {
                    QTextLayout::FormatRange o;
                    o.start = relativePos;
                    o.length = 1;
                    o.format.setForeground(palette().base());
                    selections.append(o);
                }
con's avatar
con committed
            layout->draw(&painter, offset, selections, er);

            if ((drawCursor && !drawCursorAsBlock)
                || (editable && context.cursorPosition < -1
                    && !layout->preeditAreaText().isEmpty())) {
                int cpos = context.cursorPosition;
                if (cpos < -1)
                    cpos = layout->preeditAreaPosition() - (cpos + 2);
                else
                    cpos -= blpos;
                cursor_layout = layout;
                cursor_offset = offset;
                cursor_cpos = cpos;
                cursor_pen = painter.pen();
con's avatar
con committed
            }
        }

        offset.ry() += r.height();

        if (offset.y() > viewportRect.height())
            break;
con's avatar
con committed
        block = block.next();
con's avatar
con committed
        if (!block.isVisible()) {
            if (block.blockNumber() == d->visibleCollapsedBlockNumber) {
                visibleCollapsedBlock = block;
mae's avatar
mae committed
                visibleCollapsedBlockOffset = offset + QPointF(0,1);
con's avatar
con committed
            }

            // invisible blocks do have zero line count
            block = doc->findBlockByLineNumber(block.firstLineNumber());
        }
    }
mae's avatar
mae committed
    painter.setPen(context.palette.text().color());
con's avatar
con committed

    if (backgroundVisible() && !block.isValid() && offset.y() <= er.bottom()
        && (centerOnScroll() || verticalScrollBar()->maximum() == verticalScrollBar()->minimum())) {
        painter.fillRect(QRect(QPoint((int)er.left(), (int)offset.y()), er.bottomRight()), palette().background());
    }

    //end QPlainTextEdit::paintEvent()

    delete blockSelection;

    offset = contentOffset();
    block = firstVisibleBlock();

    int top = (int)blockBoundingGeometry(block).translated(offset).top();
    int bottom = top + (int)blockBoundingRect(block).height();

    QTextCursor cursor = textCursor();
    bool hasSelection = cursor.hasSelection();
    int selectionStart = cursor.selectionStart();
    int selectionEnd = cursor.selectionEnd();


    while (block.isValid() && top <= e->rect().bottom()) {
        QTextBlock nextBlock = block.next();
        QTextBlock nextVisibleBlock = nextBlock;

        if (!nextVisibleBlock.isVisible()) {
con's avatar
con committed
            // invisible blocks do have zero line count
            nextVisibleBlock = doc->findBlockByLineNumber(nextVisibleBlock.firstLineNumber());
            // paranoia in case our code somewhere did not set the line count
            // of the invisible block to 0
            while (nextVisibleBlock.isValid() && !nextVisibleBlock.isVisible())
                nextVisibleBlock = nextVisibleBlock.next();
        }
con's avatar
con committed
        if (block.isVisible() && bottom >= e->rect().top()) {
            if (d->m_displaySettings.m_visualizeWhitespace) {
                QTextLayout *layout = block.layout();
                int lineCount = layout->lineCount();
                if (lineCount >= 2 || !nextBlock.isValid()) {
                    painter.save();
                    painter.setPen(Qt::lightGray);
                    for (int i = 0; i < lineCount-1; ++i) { // paint line wrap indicator
                        QTextLine line = layout->lineAt(i);
                        QRectF lineRect = line.naturalTextRect().translated(offset.x(), top);
                        QChar visualArrow((ushort)0x21b5);
                        painter.drawText(static_cast<int>(lineRect.right()),
                                         static_cast<int>(lineRect.top() + line.ascent()), visualArrow);
                    }
                    if (!nextBlock.isValid()) { // paint EOF symbol
                        QTextLine line = layout->lineAt(lineCount-1);
                        QRectF lineRect = line.naturalTextRect().translated(offset.x(), top);
                        int h = 4;
                        lineRect.adjust(0, 0, -1, -1);
                        QPainterPath path;
                        QPointF pos(lineRect.topRight() + QPointF(h+4, line.ascent()));
                        path.moveTo(pos);
                        path.lineTo(pos + QPointF(-h, -h));
                        path.lineTo(pos + QPointF(0, -2*h));
                        path.lineTo(pos + QPointF(h, -h));
                        path.closeSubpath();
                        painter.setBrush(painter.pen().color());
                        painter.drawPath(path);
                    }
                    painter.restore();
                }
            }

            if (nextBlock.isValid() && !nextBlock.isVisible()) {

                bool selectThis = (hasSelection
                                   && nextBlock.position() >= selectionStart
                                   && nextBlock.position() < selectionEnd);
                if (selectThis) {
                    painter.save();
                    painter.setBrush(palette().highlight());
                }

                QTextLayout *layout = block.layout();
                QTextLine line = layout->lineAt(layout->lineCount()-1);
                QRectF lineRect = line.naturalTextRect().translated(offset.x(), top);
                lineRect.adjust(0, 0, -1, -1);

                QRectF collapseRect(lineRect.right() + 12,
                                    lineRect.top(),
                                    fontMetrics().width(QLatin1String(" {...}; ")),
                                    lineRect.height());
                painter.setRenderHint(QPainter::Antialiasing, true);
                painter.translate(.5, .5);
                painter.drawRoundedRect(collapseRect.adjusted(0, 0, 0, -1), 3, 3);
                painter.setRenderHint(QPainter::Antialiasing, false);
                painter.translate(-.5, -.5);

                QString replacement = QLatin1String("...");

                QTextBlock info = block;
                if (block.userData()
                    && static_cast<TextBlockUserData*>(block.userData())->collapseMode() == TextBlockUserData::CollapseAfter)
                    ;
                else if (block.next().userData()
                         && static_cast<TextBlockUserData*>(block.next().userData())->collapseMode()
                         == TextBlockUserData::CollapseThis) {
                    replacement.prepend(nextBlock.text().trimmed().left(1));
                    info = nextBlock;
                }


                block = nextVisibleBlock.previous();
                if (!block.isValid())
                    block = doc->lastBlock();

                if (info.userData()
                    && static_cast<TextBlockUserData*>(info.userData())->collapseIncludesClosure()) {
                    QString right = block.text().trimmed();
                    if (right.endsWith(QLatin1Char(';'))) {
                        right.chop(1);
                        right = right.trimmed();
                        replacement.append(right.right(right.endsWith(QLatin1Char('/')) ? 2 : 1));
                        replacement.append(QLatin1Char(';'));
                    } else {
                        replacement.append(right.right(right.endsWith(QLatin1Char('/')) ? 2 : 1));
                    }
                }
                if (selectThis)
                    painter.setPen(palette().highlightedText().color());
                painter.drawText(collapseRect, Qt::AlignCenter, replacement);
                if (selectThis)
                    painter.restore();
            }
        }

        block = nextVisibleBlock;
        top = bottom;
        bottom = top + (int)blockBoundingRect(block).height();
    }

    if (visibleCollapsedBlock.isValid() ) {
        int margin = doc->documentMargin();
        qreal maxWidth = 0;
        qreal blockHeight = 0;
        QTextBlock b = visibleCollapsedBlock;

        while (!b.isVisible()) {
con's avatar
con committed
            b.setVisible(true); // make sure block bounding rect works
            QRectF r = blockBoundingRect(b).translated(visibleCollapsedBlockOffset);

            QTextLayout *layout = b.layout();
            for (int i = layout->lineCount()-1; i >= 0; --i)
                maxWidth = qMax(maxWidth, layout->lineAt(i).naturalTextWidth() + 2*margin);
con's avatar
con committed

            blockHeight += r.height();

            b.setVisible(false); // restore previous state
            b.setLineCount(0); // restore 0 line count for invisible block
con's avatar
con committed
            b = b.next();
        }

        painter.save();
        painter.setRenderHint(QPainter::Antialiasing, true);
        painter.translate(.5, .5);
        QBrush brush = baseColor;
        if (d->m_ifdefedOutFormat.hasProperty(QTextFormat::BackgroundBrush))
            brush = d->m_ifdefedOutFormat.background();
        painter.setBrush(brush);
con's avatar
con committed
        painter.drawRoundedRect(QRectF(visibleCollapsedBlockOffset.x(),
                                       visibleCollapsedBlockOffset.y(),
mae's avatar
mae committed
                                       maxWidth, blockHeight).adjusted(0, 0, 0, 0), 3, 3);
con's avatar
con committed
        painter.restore();

        QTextBlock end = b;
        b = visibleCollapsedBlock;
        while (b != end) {
            b.setVisible(true); // make sure block bounding rect works
            QRectF r = blockBoundingRect(b).translated(visibleCollapsedBlockOffset);
            QTextLayout *layout = b.layout();
            QVector<QTextLayout::FormatRange> selections;
            layout->draw(&painter, visibleCollapsedBlockOffset, selections, er);

            b.setVisible(false); // restore previous state
            visibleCollapsedBlockOffset.ry() += r.height();
            b = b.next();
        }
    }

mae's avatar
mae committed
    if (d->m_animator && d->m_animator->isRunning()) {
        QTextCursor cursor = textCursor();
        cursor.setPosition(d->m_animator->position());
        d->m_animator->draw(&painter, cursorRect(cursor).topLeft());
    }

    if (d->m_overlay && d->m_overlay->isVisible())
        d->m_overlay->paint(&painter, e->rect());

    if (d->m_snippetOverlay && d->m_snippetOverlay->isVisible())
        d->m_snippetOverlay->paint(&painter, e->rect());
    if (!d->m_searchResultOverlay->isEmpty()) {
        d->m_searchResultOverlay->paint(&painter, e->rect());
        d->m_searchResultOverlay->clear();
    }

    // draw the cursor last, on top of everything
    if (cursor_layout) {
        painter.setPen(cursor_pen);
        cursor_layout->drawCursor(&painter, cursor_offset, cursor_cpos, cursorWidth());
    }

con's avatar
con committed
}

QWidget *BaseTextEditor::extraArea() const
{
    return d->m_extraArea;
}

int BaseTextEditor::extraAreaWidth(int *markWidthPtr) const
{
    TextEditDocumentLayout *documentLayout = qobject_cast<TextEditDocumentLayout*>(document()->documentLayout());
    if (!documentLayout)
        return 0;

    if (!d->m_marksVisible && documentLayout->hasMarks)
        d->m_marksVisible = true;

    int space = 0;
    const QFontMetrics fm(d->m_extraArea->fontMetrics());

    if (d->m_lineNumbersVisible) {
        QFont fnt = d->m_extraArea->font();
        // this works under the assumption that bold or italic can only make a font wider
        fnt.setBold(d->m_currentLineNumberFormat.font().bold());
        fnt.setItalic(d->m_currentLineNumberFormat.font().italic());
        const QFontMetrics linefm(fnt);

con's avatar
con committed
        int digits = 2;
        int max = qMax(1, blockCount());
        while (max >= 100) {
            max /= 10;
            ++digits;
        }
        space += linefm.width(QLatin1Char('9')) * digits;
con's avatar
con committed
    }
    int markWidth = 0;

    if (d->m_marksVisible) {
        markWidth += fm.lineSpacing();
//     if (documentLayout->doubleMarkCount)
//         markWidth += fm.lineSpacing() / 3;
        space += markWidth;
    } else {
        space += 2;
    }

    if (markWidthPtr)
        *markWidthPtr = markWidth;

    space += 4;

    if (d->m_codeFoldingVisible)
        space += collapseBoxWidth(fm);
con's avatar
con committed
    return space;
}

void BaseTextEditor::slotUpdateExtraAreaWidth()
con's avatar
con committed
{
    if (isLeftToRight())
        setViewportMargins(extraAreaWidth(), 0, 0, 0);
    else
        setViewportMargins(0, 0, extraAreaWidth(), 0);
con's avatar
con committed
}

static void drawRectBox(QPainter *painter, const QRect &rect, bool start, bool end,
                        const QPalette &pal)
{
    painter->save();
    painter->setRenderHint(QPainter::Antialiasing, false);

    QRgb b = pal.base().color().rgb();
    QRgb h = pal.highlight().color().rgb();
    QColor c = Utils::StyleHelper::mergedColors(b,h, 50);
    QLinearGradient grad(rect.topLeft(), rect.topRight());
    grad.setColorAt(0, c.lighter(110));
    grad.setColorAt(1, c.lighter(130));
    QRect r = rect;

    painter->fillRect(rect, grad);
    painter->setPen(outline);
    if (start)
        painter->drawLine(rect.topLeft() + QPoint(1, 0), rect.topRight() -  QPoint(1, 0));
    if (end)
        painter->drawLine(rect.bottomLeft() + QPoint(1, 0), rect.bottomRight() -  QPoint(1, 0));

    painter->drawLine(rect.topRight() + QPoint(0, start ? 1 : 0), rect.bottomRight() - QPoint(0, end ? 1 : 0));
    painter->drawLine(rect.topLeft() + QPoint(0, start ? 1 : 0), rect.bottomLeft() - QPoint(0, end ? 1 : 0));

    painter->restore();
con's avatar
con committed
void BaseTextEditor::extraAreaPaintEvent(QPaintEvent *e)
{
    QTextDocument *doc = document();
    TextEditDocumentLayout *documentLayout = qobject_cast<TextEditDocumentLayout*>(doc->documentLayout());
hjk's avatar
hjk committed
    QTC_ASSERT(documentLayout, return);
con's avatar
con committed

mae's avatar
mae committed
    int selStart = textCursor().selectionStart();
    int selEnd = textCursor().selectionEnd();

    const QColor baseColor = palette().base().color();
con's avatar
con committed
    QPalette pal = d->m_extraArea->palette();
    pal.setCurrentColorGroup(QPalette::Active);
    QPainter painter(d->m_extraArea);
    const QFontMetrics fm(d->m_extraArea->font());
con's avatar
con committed
    int fmLineSpacing = fm.lineSpacing();

    int markWidth = 0;
    if (d->m_marksVisible)
        markWidth += fm.lineSpacing();

    const int collapseColumnWidth = d->m_codeFoldingVisible ? collapseBoxWidth(fm): 0;
    const int extraAreaWidth = d->m_extraArea->width() - collapseColumnWidth;
con's avatar
con committed

    painter.fillRect(e->rect(), pal.color(QPalette::Base));
    painter.fillRect(e->rect().intersected(QRect(0, 0, extraAreaWidth, INT_MAX)),
                     pal.color(QPalette::Background));

    QTextBlock block = firstVisibleBlock();
    int blockNumber = block.blockNumber();
    qreal top = blockBoundingGeometry(block).translated(contentOffset()).top();
    qreal bottom = top;
con's avatar
con committed

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

        top = bottom;
        bottom = top + blockBoundingRect(block).height();
con's avatar
con committed
        QTextBlock nextBlock = block.next();

        QTextBlock nextVisibleBlock = nextBlock;
        int nextVisibleBlockNumber = blockNumber + 1;

        if (!nextVisibleBlock.isVisible()) {
            // invisible blocks do have zero line count
            nextVisibleBlock = doc->findBlockByLineNumber(nextVisibleBlock.firstLineNumber());
            nextVisibleBlockNumber = nextVisibleBlock.blockNumber();
        }

mae's avatar
mae committed
        if (bottom < e->rect().top()) {
            block = nextVisibleBlock;
            blockNumber = nextVisibleBlockNumber;
            continue;
        }

con's avatar
con committed
        painter.setPen(pal.color(QPalette::Dark));

        if (d->m_codeFoldingVisible || d->m_marksVisible) {
            painter.save();
            painter.setRenderHint(QPainter::Antialiasing, false);

            int previousBraceDepth = block.previous().userState();
            if (previousBraceDepth >= 0)
                previousBraceDepth >>= 8;
            else
                previousBraceDepth = 0;

            int braceDepth = block.userState();
            if (!nextBlock.isVisible()) {
                QTextBlock lastInvisibleBlock = nextVisibleBlock.previous();
                if (!lastInvisibleBlock.isValid())
                    lastInvisibleBlock = doc->lastBlock();
                braceDepth = lastInvisibleBlock.userState();
            }
            if (braceDepth >= 0)
                braceDepth >>= 8;
            else
                braceDepth = 0;

            if (TextBlockUserData *userData = static_cast<TextBlockUserData*>(block.userData())) {
                if (d->m_marksVisible) {
                    int xoffset = 0;
                    foreach (ITextMark *mrk, userData->marks()) {
                        int x = 0;
                        int radius = fmLineSpacing - 1;
                        QRect r(x + xoffset, top, radius, radius);
                        mrk->icon().paint(&painter, r, Qt::AlignCenter);
                        xoffset += 2;
                    }
                }
            }

            if (d->m_codeFoldingVisible) {

                bool collapseThis = false;
                bool collapseAfter = false;
                bool hasClosingCollapse = false;

                if (TextBlockUserData *userData = static_cast<TextBlockUserData*>(block.userData())) {
                    if (!userData->ifdefedOut()) {
                        collapseAfter = (userData->collapseMode() == TextBlockUserData::CollapseAfter);
                        collapseThis = (userData->collapseMode() == TextBlockUserData::CollapseThis);
                        hasClosingCollapse = userData->hasClosingCollapse() && (previousBraceDepth > 0);
                    }
                }

                int extraAreaHighlightCollapseBlockNumber = -1;
                int extraAreaHighlightCollapseEndBlockNumber = -1;
                bool endIsVisible = false;
                if (!d->m_highlightBlocksInfo.isEmpty()) {
                    extraAreaHighlightCollapseBlockNumber =  d->m_highlightBlocksInfo.open.last();
                    extraAreaHighlightCollapseEndBlockNumber =  d->m_highlightBlocksInfo.close.first();
                    endIsVisible = doc->findBlockByNumber(extraAreaHighlightCollapseEndBlockNumber).isVisible();

mae's avatar
mae committed
                    QTextBlock before = doc->findBlockByNumber(extraAreaHighlightCollapseBlockNumber-1);
                    if (TextBlockUserData::hasCollapseAfter(before)) {
                        extraAreaHighlightCollapseBlockNumber--;
                    }
                }

                TextBlockUserData *nextBlockUserData = TextEditDocumentLayout::testUserData(nextBlock);

                bool collapseNext = nextBlockUserData
                                    && nextBlockUserData->collapseMode() == TextBlockUserData::CollapseThis
                                    && !nextBlockUserData->ifdefedOut();

                bool nextHasClosingCollapse = nextBlockUserData
                                              && nextBlockUserData->hasClosingCollapseInside()
                                              && nextBlockUserData->ifdefedOut();

                bool drawBox = ((collapseAfter || collapseNext) && !nextHasClosingCollapse);
                bool active = blockNumber == extraAreaHighlightCollapseBlockNumber;
                bool drawStart = drawBox && active;
                bool drawEnd = blockNumber == extraAreaHighlightCollapseEndBlockNumber || (drawStart && !endIsVisible);
                bool hovered = blockNumber >= extraAreaHighlightCollapseBlockNumber
                               && blockNumber <= extraAreaHighlightCollapseEndBlockNumber;
                int boxWidth = collapseBoxWidth(fm);
                    QRect box = QRect(extraAreaWidth + 1, top, boxWidth - 2, bottom - top);
                    drawRectBox(&painter, box, drawStart, drawEnd, pal);
                    bool expanded = nextBlock.isVisible();
                    int size = boxWidth/4;
                    QRect box(extraAreaWidth + size, top + size,
                              2 * (size) + 1, 2 * (size) + 1);
                    drawFoldingMarker(&painter, pal, box, expanded, active, hovered);
con's avatar
con committed
            }

            painter.restore();
        }


        if (d->m_revisionsVisible && block.revision() != documentLayout->lastSaveRevision) {
            painter.save();
            painter.setRenderHint(QPainter::Antialiasing, false);
            if (block.revision() < 0)
                painter.setPen(QPen(Qt::darkGreen, 2));
            else
                painter.setPen(QPen(Qt::red, 2));
            painter.drawLine(extraAreaWidth - 1, top, extraAreaWidth - 1, bottom - 1);
con's avatar
con committed
            painter.restore();
        }

        if (d->m_lineNumbersVisible) {
            const QString &number = QString::number(blockNumber + 1);
mae's avatar
mae committed
            bool selected = (
                    (selStart < block.position() + block.length()
                    && selEnd > block.position())
                    || (selStart == selEnd && selStart == block.position())
mae's avatar
mae committed
                    );
            if (selected) {
                QFont f = painter.font();
                f.setBold(d->m_currentLineNumberFormat.font().bold());
                f.setItalic(d->m_currentLineNumberFormat.font().italic());
                painter.setFont(f);
                painter.setPen(d->m_currentLineNumberFormat.foreground().color());
con's avatar
con committed
            painter.drawText(markWidth, top, extraAreaWidth - markWidth - 4, fm.height(), Qt::AlignRight, number);
mae's avatar
mae committed
            if (selected)
                painter.restore();
con's avatar
con committed
        }

        block = nextVisibleBlock;
        blockNumber = nextVisibleBlockNumber;
    }
}
void BaseTextEditor::drawFoldingMarker(QPainter *painter, const QPalette &pal,
                                       const QRect &rect,
                                       bool expanded,
                                       bool active,
                                       bool hovered) const
    Q_UNUSED(active)
    Q_UNUSED(hovered)
    QStyle *s = style();
    if (ManhattanStyle *ms = qobject_cast<ManhattanStyle*>(s))
        s = ms->baseStyle();

    if (!qstrcmp(s->metaObject()->className(), "OxygenStyle")) {
        painter->save();
        painter->setPen(Qt::NoPen);
        int size = rect.size().width();
        int sqsize = 2*(size/2);

        QColor textColor = pal.buttonText().color();
        QColor brushColor = textColor;

        textColor.setAlpha(100);
        brushColor.setAlpha(100);

        QPolygon a;
        if (expanded) {
            // down arrow
            a.setPoints(3, 0, sqsize/3,  sqsize/2, sqsize  - sqsize/3,  sqsize, sqsize/3);
        } else {
            // right arrow
            a.setPoints(3, sqsize - sqsize/3, sqsize/2,  sqsize/2 - sqsize/3, 0,  sqsize/2 - sqsize/3, sqsize);
            painter->setBrush(brushColor);
        }
        painter->translate(0.5, 0.5);
        painter->setRenderHint(QPainter::Antialiasing);
        painter->translate(rect.topLeft());
        painter->setPen(textColor);
        painter->setBrush(textColor);
        painter->drawPolygon(a);
        painter->restore();
        QStyleOptionViewItemV2 opt;
        opt.rect = rect;
        opt.state = QStyle::State_Active | QStyle::State_Item | QStyle::State_Children;
        if (expanded)
            opt.state |= QStyle::State_Open;
        if (active)
            opt.state |= QStyle::State_MouseOver | QStyle::State_Enabled | QStyle::State_Selected;
        if (hovered)
            opt.palette.setBrush(QPalette::Window, pal.highlight());

         // QGtkStyle needs a small correction to draw the marker in the right place
        if (!qstrcmp(s->metaObject()->className(), "QGtkStyle"))
           opt.rect.translate(-2, 0);
        else if (!qstrcmp(s->metaObject()->className(), "QMacStyle"))
            opt.rect.translate(-1, 0);

        s->drawPrimitive(QStyle::PE_IndicatorBranch, &opt, painter, this);
con's avatar
con committed
}

void BaseTextEditor::slotModificationChanged(bool m)
{
    if (m)
        return;

    QTextDocument *doc = document();
    TextEditDocumentLayout *documentLayout = qobject_cast<TextEditDocumentLayout*>(doc->documentLayout());
    QTC_ASSERT(documentLayout, return);
    int oldLastSaveRevision = documentLayout->lastSaveRevision;
    documentLayout->lastSaveRevision = doc->revision();

    if (oldLastSaveRevision != documentLayout->lastSaveRevision) {
        QTextBlock block = doc->begin();
        while (block.isValid()) {
            if (block.revision() < 0 || block.revision() != oldLastSaveRevision) {
                block.setRevision(-documentLayout->lastSaveRevision - 1);
            } else {
                block.setRevision(documentLayout->lastSaveRevision);
            }
            block = block.next();
        }
    }
    d->m_extraArea->update();
}

void BaseTextEditor::slotUpdateRequest(const QRect &r, int dy)
{
    if (dy)
        d->m_extraArea->scroll(0, dy);
    else if (r.width() > 4) { // wider than cursor width, not just cursor blinking
        d->m_extraArea->update(0, r.y(), d->m_extraArea->width(), r.height());
mae's avatar
mae committed
        if (!d->m_searchExpr.isEmpty()) {
            const int m = d->m_searchResultOverlay->dropShadowWidth();
            viewport()->update(r.adjusted(-m, -m, m, m));
        }
    }

    if (r.contains(viewport()->rect()))
        slotUpdateExtraAreaWidth();
}

void BaseTextEditor::saveCurrentCursorPositionForNavigation()
{
    d->m_lastCursorChangeWasInteresting = true;
    d->m_tempNavigationState = saveState();
void BaseTextEditor::updateCurrentLineHighlight()
{
    QList<QTextEdit::ExtraSelection> extraSelections;

    if (d->m_highlightCurrentLine) {
        QTextEdit::ExtraSelection sel;
        sel.format.setBackground(d->m_currentLineFormat.background());
        sel.format.setProperty(QTextFormat::FullWidthSelection, true);
        sel.cursor = textCursor();
        sel.cursor.clearSelection();
        extraSelections.append(sel);
    }

    setExtraSelections(CurrentLineSelection, extraSelections);


    // the extra area shows information for the entire current block, not just the currentline.
    // This is why we must force a bigger update region.
    int cursorBlockNumber = textCursor().blockNumber();
    if (cursorBlockNumber != d->m_cursorBlockNumber) {
        QPointF offset = contentOffset();
        QTextBlock block = document()->findBlockByNumber(d->m_cursorBlockNumber);
        if (block.isValid())
            d->m_extraArea->update(blockBoundingGeometry(block).translated(offset).toAlignedRect());
        block = document()->findBlockByNumber(cursorBlockNumber);
        if (block.isValid())
            d->m_extraArea->update(blockBoundingGeometry(block).translated(offset).toAlignedRect());
        d->m_cursorBlockNumber = cursorBlockNumber;
    }

void BaseTextEditor::slotCursorPositionChanged()
{
#if 0
    qDebug() << "block" << textCursor().blockNumber()+1
            << "depth:" << TextEditDocumentLayout::braceDepth(textCursor().block())
            << '/' << TextEditDocumentLayout::braceDepth(document()->lastBlock());
    if (!d->m_contentsChanged && d->m_lastCursorChangeWasInteresting) {
        Core::EditorManager::instance()->addCurrentPositionToNavigationHistory(editableInterface(), d->m_tempNavigationState);
        d->m_lastCursorChangeWasInteresting = false;
    } else if (d->m_contentsChanged) {
        saveCurrentCursorPositionForNavigation();
    }
con's avatar
con committed
    updateHighlights();
}
con's avatar
con committed
void BaseTextEditor::updateHighlights()
{
    if (d->m_parenthesesMatchingEnabled && hasFocus()) {
        // Delay update when no matching is displayed yet, to avoid flicker
        if (extraSelections(ParenthesesMatchingSelection).isEmpty()
            && d->m_animator == 0) {
            d->m_parenthesesMatchingTimer->start(50);
        } else {
             // use 0-timer, not direct call, to give the syntax highlighter a chance
            // to update the parantheses information
            d->m_parenthesesMatchingTimer->start(0);
    if (d->m_displaySettings.m_highlightBlocks) {
        QTextCursor cursor = textCursor();
        d->extraAreaHighlightCollapseBlockNumber = cursor.blockNumber();
        d->extraAreaHighlightCollapseColumn = cursor.position() - cursor.block().position();
        d->m_highlightBlocksTimer->start(100);
    }
}

void BaseTextEditor::slotUpdateBlockNotify(const QTextBlock &block)
{
    static bool blockRecursion = false;
    if (blockRecursion)
        return;
    blockRecursion = true;
    if (d->m_overlay->isVisible()) {
        /* an overlay might draw outside the block bounderies, force
           complete viewport update */
        viewport()->update();
    } else {
        if (block.previous().isValid() && block.userState() != block.previous().userState()) {
        /* The syntax highlighting state changes. This opens up for
           the possibility that the paragraph has braces that support
           code folding. In this case, do the save thing and also
           update the previous block, which might contain a collapse
           box which now is invalid.*/
            emit requestBlockUpdate(block.previous());
        }
mae's avatar
mae committed
        if (!d->m_findScopeStart.isNull()) {
            if (block.position() < d->m_findScopeEnd.position()
                && block.position()+block.length() >= d->m_findScopeStart.position()) {
                QTextBlock b = block.document()->findBlock(d->m_findScopeStart.position());
                do {
                    emit requestBlockUpdate(b);
                    b = b.next();
mae's avatar
mae committed
                } while (b.isValid() && b.position() < d->m_findScopeEnd.position());
    blockRecursion = false;
con's avatar
con committed
void BaseTextEditor::timerEvent(QTimerEvent *e)
{
    if (e->timerId() == d->autoScrollTimer.timerId()) {
        const QPoint globalPos = QCursor::pos();
        const QPoint pos = d->m_extraArea->mapFromGlobal(globalPos);
        QRect visible = d->m_extraArea->rect();
        verticalScrollBar()->triggerAction( pos.y() < visible.center().y() ?
                                            QAbstractSlider::SliderSingleStepSub
                                            : QAbstractSlider::SliderSingleStepAdd);
        QMouseEvent ev(QEvent::MouseMove, pos, globalPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
        extraAreaMouseEvent(&ev);
        int delta = qMax(pos.y() - visible.top(), visible.bottom() - pos.y()) - visible.height();
        if (delta < 7)
            delta = 7;
        int timeout = 4900 / (delta * delta);
        d->autoScrollTimer.start(timeout, this);

    } else if (e->timerId() == d->collapsedBlockTimer.timerId()) {
        d->visibleCollapsedBlockNumber = d->suggestedVisibleCollapsedBlockNumber;
        d->suggestedVisibleCollapsedBlockNumber = -1;
        d->collapsedBlockTimer.stop();
        viewport()->update();
    }
    QPlainTextEdit::timerEvent(e);
}


void BaseTextEditorPrivate::clearVisibleCollapsedBlock()
{
    if (suggestedVisibleCollapsedBlockNumber) {
        suggestedVisibleCollapsedBlockNumber = -1;
        collapsedBlockTimer.stop();
    }
    if (visibleCollapsedBlockNumber >= 0) {
        visibleCollapsedBlockNumber = -1;
        q->viewport()->update();
    }
}

void BaseTextEditor::mouseMoveEvent(QMouseEvent *e)
{
    d->m_lastEventWasBlockSelectionEvent = (e->modifiers() & Qt::AltModifier);
    if (e->buttons() == Qt::NoButton) {
        const QTextBlock collapsedBlock = collapsedBlockAt(e->pos());
        const int blockNumber = collapsedBlock.next().blockNumber();
con's avatar
con committed
        if (blockNumber < 0) {
            d->clearVisibleCollapsedBlock();
        } else if (blockNumber != d->visibleCollapsedBlockNumber) {
            d->suggestedVisibleCollapsedBlockNumber = blockNumber;
            d->collapsedBlockTimer.start(40, this);
        }

        // Update the mouse cursor
        if (collapsedBlock.isValid() && !d->m_mouseOnCollapsedMarker) {
            d->m_mouseOnCollapsedMarker = true;
            viewport()->setCursor(Qt::PointingHandCursor);
        } else if (!collapsedBlock.isValid() && d->m_mouseOnCollapsedMarker) {
            d->m_mouseOnCollapsedMarker = false;
            viewport()->setCursor(Qt::IBeamCursor);
        }
con's avatar
con committed
    } else {
        QPlainTextEdit::mouseMoveEvent(e);
    }
    if (d->m_lastEventWasBlockSelectionEvent && d->m_inBlockSelectionMode) {
        if (textCursor().atBlockEnd()) {
            d->m_blockSelectionExtraX = qMax(0, e->pos().x() - cursorRect().center().x()) / fontMetrics().averageCharWidth();
con's avatar
con committed
        } else {
            d->m_blockSelectionExtraX = 0;
        }
    }
    if (viewport()->cursor().shape() == Qt::BlankCursor)
        viewport()->setCursor(Qt::IBeamCursor);
con's avatar
con committed
}

static bool handleForwardBackwardMouseButtons(QMouseEvent *e)
{
    if (e->button() == Qt::XButton1) {
        Core::EditorManager::instance()->goBackInNavigationHistory();
        return true;
    }
    if (e->button() == Qt::XButton2) {
        Core::EditorManager::instance()->goForwardInNavigationHistory();
        return true;
    }

    return false;
}

con's avatar
con committed
void BaseTextEditor::mousePressEvent(QMouseEvent *e)
{
    if (e->button() == Qt::LeftButton) {
        d->clearBlockSelection(); // just in case, otherwise we might get strange drag and drop

        QTextBlock collapsedBlock = collapsedBlockAt(e->pos());
        if (collapsedBlock.isValid()) {
            toggleBlockVisible(collapsedBlock);
            viewport()->setCursor(Qt::IBeamCursor);
        }

        updateLink(e);

        if (d->m_currentLink.isValid())
            d->m_linkPressed = true;
con's avatar
con committed
    }

#ifdef Q_OS_LINUX
    if (handleForwardBackwardMouseButtons(e))
        return;
#endif

con's avatar
con committed
    QPlainTextEdit::mousePressEvent(e);
}

void BaseTextEditor::mouseReleaseEvent(QMouseEvent *e)
{
    if (mouseNavigationEnabled()
        && d->m_linkPressed
        && e->modifiers() & Qt::ControlModifier
        && !(e->modifiers() & Qt::ShiftModifier)
        const QTextCursor cursor = cursorForPosition(e->pos());
        if (openLink(findLinkAt(cursor))) {
            clearLink();
            return;
        }
    }

#ifndef Q_OS_LINUX
    if (handleForwardBackwardMouseButtons(e))
        return;
#endif

    QPlainTextEdit::mouseReleaseEvent(e);
}

void BaseTextEditor::leaveEvent(QEvent *e)
{
    // Clear link emulation when the mouse leaves the editor
    clearLink();
    QPlainTextEdit::leaveEvent(e);
}

void BaseTextEditor::keyReleaseEvent(QKeyEvent *e)
{
    // Clear link emulation when Ctrl is released
    if (e->key() == Qt::Key_Control)
        clearLink();

    QPlainTextEdit::keyReleaseEvent(e);
}

void BaseTextEditor::dragEnterEvent(QDragEnterEvent *e)
{
    // If the drag event contains URLs, we don't want to insert them as text
    if (e->mimeData()->hasUrls()) {
        e->ignore();
        return;
    }

    QPlainTextEdit::dragEnterEvent(e);
}

con's avatar
con committed
void BaseTextEditor::extraAreaLeaveEvent(QEvent *)
{
    // fake missing mouse move event from Qt
    QMouseEvent me(QEvent::MouseMove, QPoint(-1, -1), Qt::NoButton, 0, 0);
    extraAreaMouseEvent(&me);
con's avatar
con committed
}

void BaseTextEditor::extraAreaMouseEvent(QMouseEvent *e)
{
    QTextCursor cursor = cursorForPosition(QPoint(0, e->pos().y()));
    cursor.setPosition(cursor.block().position());

    int markWidth;
    extraAreaWidth(&markWidth);

    if (d->m_codeFoldingVisible
        && e->type() == QEvent::MouseMove && e->buttons() == 0) { // mouse tracking
        // Update which folder marker is highlighted
        const int highlightBlockNumber = d->extraAreaHighlightCollapseBlockNumber;
        const int highlightColumn = d->extraAreaHighlightCollapseColumn;
con's avatar
con committed
        d->extraAreaHighlightCollapseBlockNumber = -1;
        d->extraAreaHighlightCollapseColumn = -1;
        if (e->pos().x() > extraArea()->width() - collapseBoxWidth(fontMetrics())) {
            d->extraAreaHighlightCollapseBlockNumber = cursor.blockNumber();
mae's avatar
mae committed
            if (TextBlockUserData::canCollapse(cursor.block())
                || !TextBlockUserData::hasClosingCollapse(cursor.block()))
                d->extraAreaHighlightCollapseColumn = cursor.block().length()-1;
            if (TextBlockUserData::hasCollapseAfter(cursor.block())) {
                d->extraAreaHighlightCollapseBlockNumber++;
mae's avatar
mae committed
                d->extraAreaHighlightCollapseColumn = -1;
                if (TextBlockUserData::canCollapse(cursor.block().next())
                    || !TextBlockUserData::hasClosingCollapse(cursor.block().next()))
                    d->extraAreaHighlightCollapseColumn = cursor.block().next().length()-1;
            }
        } else if (d->m_displaySettings.m_highlightBlocks) {
            QTextCursor cursor = textCursor();
            d->extraAreaHighlightCollapseBlockNumber = cursor.blockNumber();
            d->extraAreaHighlightCollapseColumn = cursor.position() - cursor.block().position();
con's avatar
con committed
        }
        if (highlightBlockNumber != d->extraAreaHighlightCollapseBlockNumber
            || highlightColumn != d->extraAreaHighlightCollapseColumn) {
            d->m_highlightBlocksTimer->start(d->m_highlightBlocksInfo.isEmpty() ? 120 : 0);
        }
con's avatar
con committed
    }

    // Set whether the mouse cursor is a hand or normal arrow
    if (e->type() == QEvent::MouseMove) {
        bool hand = (e->pos().x() <= markWidth);
        if (hand != (d->m_extraArea->cursor().shape() == Qt::PointingHandCursor))
            d->m_extraArea->setCursor(hand ? Qt::PointingHandCursor : Qt::ArrowCursor);
    }

con's avatar
con committed
    if (e->type() == QEvent::MouseButtonPress || e->type() == QEvent::MouseButtonDblClick) {
        if (e->button() == Qt::LeftButton) {
            int boxWidth = collapseBoxWidth(fontMetrics());
            if (d->m_codeFoldingVisible && e->pos().x() > extraArea()->width() - boxWidth) {
                if (!cursor.block().next().isVisible()) {
                    toggleBlockVisible(cursor.block());
                    d->moveCursorVisible(false);
                } else if (collapseBox().contains(e->pos())) {
                    cursor.setPosition(
                            document()->findBlockByNumber(d->m_highlightBlocksInfo.open.last()).position()
                            );
                    QTextBlock c = cursor.block();
                    if (TextBlockUserData::hasCollapseAfter(c.previous()))
                        c = c.previous();
                    toggleBlockVisible(c);
                    d->moveCursorVisible(false);
                }
            } else if (d->m_marksVisible && e->pos().x() > markWidth) {
con's avatar
con committed
                QTextCursor selection = cursor;
                selection.setVisualNavigation(true);
                d->extraAreaSelectionAnchorBlockNumber = selection.blockNumber();
                selection.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
                selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
                setTextCursor(selection);
            } else {
                d->extraAreaToggleMarkBlockNumber = cursor.blockNumber();
            }
        } else if (d->m_marksVisible && e->button() == Qt::RightButton) {
            QMenu * contextMenu = new QMenu(this);
mae's avatar
mae committed
            emit d->m_editable->markContextMenuRequested(editableInterface(), cursor.blockNumber() + 1, contextMenu);
            if (!contextMenu->isEmpty())
                contextMenu->exec(e->globalPos());
            delete contextMenu;
con's avatar
con committed
        }
    } else if (d->extraAreaSelectionAnchorBlockNumber >= 0) {
        QTextCursor selection = cursor;
        selection.setVisualNavigation(true);
        if (e->type() == QEvent::MouseMove) {
            QTextBlock anchorBlock = document()->findBlockByNumber(d->extraAreaSelectionAnchorBlockNumber);
            selection.setPosition(anchorBlock.position());
            if (cursor.blockNumber() < d->extraAreaSelectionAnchorBlockNumber) {
                selection.movePosition(QTextCursor::EndOfBlock);
                selection.movePosition(QTextCursor::Right);
            }
            selection.setPosition(cursor.block().position(), QTextCursor::KeepAnchor);
            if (cursor.blockNumber() >= d->extraAreaSelectionAnchorBlockNumber) {
                selection.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
                selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
            }

            if (e->pos().y() >= 0 && e->pos().y() <= d->m_extraArea->height())
                d->autoScrollTimer.stop();
            else if (!d->autoScrollTimer.isActive())
                d->autoScrollTimer.start(100, this);

        } else {
            d->autoScrollTimer.stop();
            d->extraAreaSelectionAnchorBlockNumber = -1;
            return;
        }
        setTextCursor(selection);
    } else if (d->extraAreaToggleMarkBlockNumber >= 0 && d->m_marksVisible && d->m_requestMarkEnabled) {
        if (e->type() == QEvent::MouseButtonRelease && e->button() == Qt::LeftButton) {
            int n = d->extraAreaToggleMarkBlockNumber;
            d->extraAreaToggleMarkBlockNumber = -1;
            if (cursor.blockNumber() == n) {
                int line = n + 1;
                emit d->m_editable->markRequested(editableInterface(), line);
con's avatar
con committed
            }
        }
    }
}

QTextBlock TextBlockUserData::testCollapse(const QTextBlock& block)
{
    QTextBlock info = block;
    if (block.userData() && static_cast<TextBlockUserData*>(block.userData())->collapseMode() == CollapseAfter)
        ;
    else if (block.next().userData()
             && static_cast<TextBlockUserData*>(block.next().userData())->collapseMode()
             == TextBlockUserData::CollapseThis)
        info = block.next();
    else
        return QTextBlock();
    int pos = static_cast<TextBlockUserData*>(info.userData())->collapseAtPos();
    if (pos < 0)
        return QTextBlock();
    QTextCursor cursor(info);
    cursor.setPosition(cursor.position() + pos);
    matchCursorForward(&cursor);
    return cursor.block();
}

void TextBlockUserData::doCollapse(const QTextBlock& block, bool visible)
{
    QTextBlock info = block;
    if (block.userData() && static_cast<TextBlockUserData*>(block.userData())->collapseMode() == CollapseAfter)
        ;
    else if (block.next().userData()
             && static_cast<TextBlockUserData*>(block.next().userData())->collapseMode()
             == TextBlockUserData::CollapseThis)
        info = block.next();
    else {
        if (visible && !block.next().isVisible()) {
            // no match, at least unfold!
            QTextBlock b = block.next();
            while (b.isValid() && !b.isVisible()) {
                b.setVisible(true);
                b.setLineCount(visible ? qMax(1, b.layout()->lineCount()) : 0);
                b = b.next();
            }
        }
        return;
    }
    int pos = static_cast<TextBlockUserData*>(info.userData())->collapseAtPos();
    if (pos < 0)
        return;
    QTextCursor cursor(info);
    cursor.setPosition(cursor.position() + pos);
    if (matchCursorForward(&cursor) != Match) {
        if (visible) {
            // no match, at least unfold!
            QTextBlock b = block.next();
            while (b.isValid() && !b.isVisible()) {
                b.setVisible(true);
                b.setLineCount(visible ? qMax(1, b.layout()->lineCount()) : 0);
                b = b.next();
            }
        }
        return;
    }

    QTextBlock b = block.next();
    while (b < cursor.block()) {
        b.setVisible(visible);
        b.setLineCount(visible ? qMax(1, b.layout()->lineCount()) : 0);
        if (visible) {
            TextBlockUserData *data = canCollapse(b);
            if (data && data->collapsed()) {
                QTextBlock end =  testCollapse(b);
                if (data->collapseIncludesClosure())
                    end = end.next();
                if (end.isValid()) {
                    b = end;
                    continue;
                }
            }
        }
        b = b.next();
    }

    bool collapseIncludesClosure = hasClosingCollapseAtEnd(b);
    if (collapseIncludesClosure) {
        b.setVisible(visible);
        b.setLineCount(visible ? qMax(1, b.layout()->lineCount()) : 0);
    }
    static_cast<TextBlockUserData*>(info.userData())->setCollapseIncludesClosure(collapseIncludesClosure);
    static_cast<TextBlockUserData*>(info.userData())->setCollapsed(!block.next().isVisible());

}


void BaseTextEditor::ensureCursorVisible()
{
    QTextBlock block = textCursor().block();
    if (!block.isVisible()) {
        while (!block.isVisible() && block.previous().isValid())
            block = block.previous();
        toggleBlockVisible(block);
    }
    QPlainTextEdit::ensureCursorVisible();
}

void BaseTextEditor::toggleBlockVisible(const QTextBlock &block)
{
    TextEditDocumentLayout *documentLayout = qobject_cast<TextEditDocumentLayout*>(document()->documentLayout());
hjk's avatar
hjk committed
    QTC_ASSERT(documentLayout, return);
con's avatar
con committed

    bool visible = block.next().isVisible();
    TextBlockUserData::doCollapse(block, !visible);
    documentLayout->requestUpdate();
    documentLayout->emitDocumentSizeChanged();
}


const TabSettings &BaseTextEditor::tabSettings() const
{
    return d->m_document->tabSettings();
}

const DisplaySettings &BaseTextEditor::displaySettings() const
{
    return d->m_displaySettings;
}


void BaseTextEditor::indentOrUnindent(bool doIndent)
{
    QTextCursor cursor = textCursor();
    cursor.beginEditBlock();

    int pos = cursor.position();
    const TextEditor::TabSettings &tabSettings = d->m_document->tabSettings();

    QTextDocument *doc = document();

    if (!cursor.hasSelection() && doIndent) {
        // Insert tab if there is no selection and indent is requested
con's avatar
con committed
        QTextBlock block = cursor.block();
        QString text = block.text();
        int indentPosition = (cursor.position() - block.position());;
        int spaces = tabSettings.spacesLeftFromPosition(text, indentPosition);
        int startColumn = tabSettings.columnAt(text, indentPosition - spaces);
        int targetColumn = tabSettings.indentedColumn(tabSettings.columnAt(text, indentPosition), doIndent);
        cursor.setPosition(block.position() + indentPosition);
        cursor.setPosition(block.position() + indentPosition - spaces, QTextCursor::KeepAnchor);
        cursor.removeSelectedText();
        cursor.insertText(tabSettings.indentationString(startColumn, targetColumn, block));
con's avatar
con committed
    } else {
        // Indent or unindent the selected lines
con's avatar
con committed
        int anchor = cursor.anchor();
        int start = qMin(anchor, pos);
        int end = qMax(anchor, pos);

        QTextBlock startBlock = doc->findBlock(start);
        QTextBlock endBlock = doc->findBlock(end-1).next();

        for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
            QString text = block.text();
            int indentPosition = tabSettings.lineIndentPosition(text);
            if (!doIndent && !indentPosition)
                indentPosition = tabSettings.firstNonSpace(text);
            int targetColumn = tabSettings.indentedColumn(tabSettings.columnAt(text, indentPosition), doIndent);
            cursor.setPosition(block.position() + indentPosition);
            cursor.insertText(tabSettings.indentationString(0, targetColumn, block));
con's avatar
con committed
            cursor.setPosition(block.position());
            cursor.setPosition(block.position() + indentPosition, QTextCursor::KeepAnchor);
            cursor.removeSelectedText();
        }
    }

    cursor.endEditBlock();
}

void BaseTextEditor::handleHomeKey(bool anchor)
{
    QTextCursor cursor = textCursor();
    QTextCursor::MoveMode mode = QTextCursor::MoveAnchor;

    if (anchor)
        mode = QTextCursor::KeepAnchor;

    const int initpos = cursor.position();
    int pos = cursor.block().position();
    QChar character = characterAt(pos);
    const QLatin1Char tab = QLatin1Char('\t');

    while (character == tab || character.category() == QChar::Separator_Space) {
        ++pos;
con's avatar
con committed
        character = characterAt(pos);
    }

    // Go to the start of the block when we're already at the start of the text
    if (pos == initpos)
        pos = cursor.block().position();

    cursor.setPosition(pos, mode);
    setTextCursor(cursor);
}

void BaseTextEditor::handleBackspaceKey()
{
    QTextCursor cursor = textCursor();
    int pos = cursor.position();
hjk's avatar
hjk committed
    QTC_ASSERT(!cursor.hasSelection(), return);
con's avatar
con committed

    if (d->m_snippetOverlay->isVisible()) {
        QTextCursor snippetCursor = cursor;
        snippetCursor.movePosition(QTextCursor::Left);
        d->snippetCheckCursor(snippetCursor);
    }

con's avatar
con committed
    const TextEditor::TabSettings &tabSettings = d->m_document->tabSettings();
    if (tabSettings.m_autoIndent && autoBackspace(cursor))

    if (!tabSettings.m_smartBackspace) {
        cursor.deletePreviousChar();
        return;
    }

con's avatar
con committed
    QTextBlock currentBlock = cursor.block();
    int positionInBlock = pos - currentBlock.position();
con's avatar
con committed
    const QString blockText = currentBlock.text();
    if (cursor.atBlockStart() || tabSettings.firstNonSpace(blockText) < positionInBlock) {
        cursor.deletePreviousChar();
        return;
    }

    int previousIndent = 0;
    const int indent = tabSettings.columnAt(blockText, positionInBlock);

    for (QTextBlock previousNonEmptyBlock = currentBlock.previous();
         previousNonEmptyBlock.isValid();
         previousNonEmptyBlock = previousNonEmptyBlock.previous()) {
        QString previousNonEmptyBlockText = previousNonEmptyBlock.text();
        if (previousNonEmptyBlockText.trimmed().isEmpty())
            continue;
        previousIndent = tabSettings.columnAt(previousNonEmptyBlockText,
                                              tabSettings.firstNonSpace(previousNonEmptyBlockText));
        if (previousIndent < indent) {
            cursor.beginEditBlock();
            cursor.setPosition(currentBlock.position(), QTextCursor::KeepAnchor);
            cursor.insertText(tabSettings.indentationString(previousNonEmptyBlockText));
            cursor.endEditBlock();
mae's avatar
mae committed
            return;
con's avatar
con committed
    }
mae's avatar
mae committed
    cursor.deletePreviousChar();
con's avatar
con committed
}

void BaseTextEditor::wheelEvent(QWheelEvent *e)
{
    d->clearVisibleCollapsedBlock();
    if (scrollWheelZoomingEnabled() && e->modifiers() & Qt::ControlModifier) {
con's avatar
con committed
        const int delta = e->delta();
        if (delta < 0)
            zoomOut();
        else if (delta > 0)
            zoomIn();
        return;
    }
    QPlainTextEdit::wheelEvent(e);
}

void BaseTextEditor::zoomIn(int range)
{
mae's avatar
mae committed
    d->clearVisibleCollapsedBlock();
    emit requestFontZoom(range*10);
con's avatar
con committed
}

void BaseTextEditor::zoomOut(int range)
{
    zoomIn(-range);
}

mae's avatar
mae committed
void BaseTextEditor::zoomReset()
{
    emit requestZoomReset();
}

con's avatar
con committed
bool BaseTextEditor::isElectricCharacter(const QChar &) const
{
    return false;
}

void BaseTextEditor::indentInsertedText(const QTextCursor &tc)
{
    indent(tc.document(), tc, QChar::Null);
}

void BaseTextEditor::countBracket(QChar open, QChar close, QChar c, int *errors, int *stillopen)
{
    if (c == open)
        ++*stillopen;
    else if (c == close)
        --*stillopen;

    if (*stillopen < 0) {
        *errors += -1 * (*stillopen);
        *stillopen = 0;
    }
}

void BaseTextEditor::countBrackets(QTextCursor cursor, int from, int end, QChar open, QChar close, int *errors, int *stillopen)
{
    cursor.setPosition(from);
    QTextBlock block = cursor.block();
    while (block.isValid() && block.position() < end) {
        TextEditor::Parentheses parenList = TextEditor::TextEditDocumentLayout::parentheses(block);
        if (!parenList.isEmpty() && !TextEditor::TextEditDocumentLayout::ifdefedOut(block)) {
            for (int i = 0; i < parenList.count(); ++i) {
                TextEditor::Parenthesis paren = parenList.at(i);
                int position = block.position() + paren.pos;
                if (position < from || position >= end)
                    continue;
                countBracket(open, close, paren.chr, errors, stillopen);
            }
        }
        block = block.next();
    }
}

bool BaseTextEditor::contextAllowsAutoParentheses(const QTextCursor &cursor,
                                                  const QString &textToInsert) const
{
    Q_UNUSED(cursor);
    Q_UNUSED(textToInsert);
    return false;
}

bool BaseTextEditor::isInComment(const QTextCursor &cursor) const
    Q_UNUSED(cursor);
    return false;
}

QString BaseTextEditor::insertMatchingBrace(const QTextCursor &tc, const QString &text,
                                            const QChar &la, int *skippedChars) const
{
    Q_UNUSED(tc);
    Q_UNUSED(text);
    Q_UNUSED(la);
    Q_UNUSED(skippedChars);
QString BaseTextEditor::insertParagraphSeparator(const QTextCursor &tc) const
{
    Q_UNUSED(tc);
    return QString();
}

QString BaseTextEditor::autoComplete(QTextCursor &cursor, const QString &textToInsert) const
{
    const bool checkBlockEnd = d->m_allowSkippingOfBlockEnd;
    d->m_allowSkippingOfBlockEnd = false; // consume blockEnd.

    if (!contextAllowsAutoParentheses(cursor, textToInsert))
        return QString();

    const QString text = textToInsert;
    const QChar lookAhead = characterAt(cursor.selectionEnd());

    QChar character = textToInsert.at(0);
    const QString parentheses = QLatin1String("()");
    const QString brackets = QLatin1String("[]");
    if (parentheses.contains(character) || brackets.contains(character)) {
        QTextCursor tmp= cursor;
con's avatar
con committed
        bool foundBlockStart = TextEditor::TextBlockUserData::findPreviousBlockOpenParenthesis(&tmp);
        int blockStart = foundBlockStart ? tmp.position() : 0;
con's avatar
con committed
        bool foundBlockEnd = TextEditor::TextBlockUserData::findNextBlockClosingParenthesis(&tmp);
        int blockEnd = foundBlockEnd ? tmp.position() : (cursor.document()->characterCount() - 1);
        const QChar openChar = parentheses.contains(character) ? QLatin1Char('(') : QLatin1Char('[');
        const QChar closeChar = parentheses.contains(character) ? QLatin1Char(')') : QLatin1Char(']');

        int errors = 0;
        int stillopen = 0;
        countBrackets(cursor, blockStart, blockEnd, openChar, closeChar, &errors, &stillopen);
        int errorsBeforeInsertion = errors + stillopen;
        errors = 0;
        stillopen = 0;
        countBrackets(cursor, blockStart, cursor.position(), openChar, closeChar, &errors, &stillopen);
        countBracket(openChar, closeChar, character, &errors, &stillopen);
        countBrackets(cursor, cursor.position(), blockEnd, openChar, closeChar, &errors, &stillopen);
        int errorsAfterInsertion = errors + stillopen;
        if (errorsAfterInsertion < errorsBeforeInsertion)
            return QString(); // insertion fixes parentheses or bracket errors, do not auto complete
    }

    int skippedChars = 0;
    const QString autoText = insertMatchingBrace(cursor, text, lookAhead, &skippedChars);

    if (checkBlockEnd && textToInsert.at(0) == QLatin1Char('}')) {
        if (textToInsert.length() > 1)
            qWarning() << "*** handle event compression";

        int startPos = cursor.selectionEnd(), pos = startPos;
        while (characterAt(pos).isSpace())
            ++pos;

        if (characterAt(pos) == QLatin1Char('}'))
            skippedChars += (pos - startPos) + 1;
    }

    if (skippedChars) {
        const int pos = cursor.position();
        cursor.setPosition(pos + skippedChars);
        cursor.setPosition(pos, QTextCursor::KeepAnchor);
    }

    return autoText;
}

bool BaseTextEditor::autoBackspace(QTextCursor &cursor)
{
    d->m_allowSkippingOfBlockEnd = false;

    int pos = cursor.position();
mae's avatar
mae committed
    if (pos == 0)
        return false;
    QTextCursor c = cursor;
    c.setPosition(pos - 1);

    QChar lookAhead = characterAt(pos);
    QChar lookBehind = characterAt(pos-1);
    QChar lookFurtherBehind = characterAt(pos-2);

    QChar character = lookBehind;
    if (character == QLatin1Char('(') || character == QLatin1Char('[')) {
        QTextCursor tmp = cursor;
        TextEditor::TextBlockUserData::findPreviousBlockOpenParenthesis(&tmp);
        int blockStart = tmp.isNull() ? 0 : tmp.position();
        tmp = cursor;
        TextEditor::TextBlockUserData::findNextBlockClosingParenthesis(&tmp);
        int blockEnd = tmp.isNull() ? (cursor.document()->characterCount()-1) : tmp.position();
        QChar openChar = character;
        QChar closeChar = (character == QLatin1Char('(')) ? QLatin1Char(')') : QLatin1Char(']');

        int errors = 0;
        int stillopen = 0;
        countBrackets(cursor, blockStart, blockEnd, openChar, closeChar, &errors, &stillopen);
        int errorsBeforeDeletion = errors + stillopen;
        errors = 0;
        stillopen = 0;
        countBrackets(cursor, blockStart, pos - 1, openChar, closeChar, &errors, &stillopen);
        countBrackets(cursor, pos, blockEnd, openChar, closeChar, &errors, &stillopen);
        int errorsAfterDeletion = errors + stillopen;

        if (errorsAfterDeletion < errorsBeforeDeletion)
            return false; // insertion fixes parentheses or bracket errors, do not auto complete
    }

Loading
Loading full blame...