Browse Source

replace VEdit and VMdEdit with VEditor and VMdEditor

Le Tan 8 years ago
parent
commit
404b5329a1

+ 10 - 2
src/src.pro

@@ -84,7 +84,11 @@ SOURCES += main.cpp\
     dialog/vinsertlinkdialog.cpp \
     vplaintextedit.cpp \
     vimageresourcemanager.cpp \
-    vlinenumberarea.cpp
+    vlinenumberarea.cpp \
+    veditor.cpp \
+    vmdeditor.cpp \
+    veditconfig.cpp \
+    vpreviewmanager.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -156,7 +160,11 @@ HEADERS  += vmainwindow.h \
     dialog/vinsertlinkdialog.h \
     vplaintextedit.h \
     vimageresourcemanager.h \
-    vlinenumberarea.h
+    vlinenumberarea.h \
+    veditor.h \
+    vmdeditor.h \
+    veditconfig.h \
+    vpreviewmanager.h
 
 RESOURCES += \
     vnote.qrc \

+ 87 - 0
src/utils/veditutils.cpp

@@ -3,6 +3,7 @@
 #include <QTextDocument>
 #include <QDebug>
 #include <QTextEdit>
+#include <QPlainTextEdit>
 #include <QScrollBar>
 
 #include "vutils.h"
@@ -418,6 +419,92 @@ void VEditUtils::scrollBlockInPage(QTextEdit *p_edit,
     p_edit->ensureCursorVisible();
 }
 
+void VEditUtils::scrollBlockInPage(QPlainTextEdit *p_edit,
+                                   int p_blockNum,
+                                   int p_dest)
+{
+    QTextDocument *doc = p_edit->document();
+    QTextCursor cursor = p_edit->textCursor();
+    if (p_blockNum >= doc->blockCount()) {
+        p_blockNum = doc->blockCount() - 1;
+    }
+
+    QTextBlock block = doc->findBlockByNumber(p_blockNum);
+
+    int pib = cursor.positionInBlock();
+    if (cursor.block().blockNumber() != p_blockNum) {
+        // Move the cursor to the block.
+        if (pib >= block.length()) {
+            pib = block.length() - 1;
+        }
+
+        cursor.setPosition(block.position() + pib);
+        p_edit->setTextCursor(cursor);
+    }
+
+    // Scroll to let current cursor locate in proper position.
+    p_edit->ensureCursorVisible();
+    QScrollBar *vsbar = p_edit->verticalScrollBar();
+
+    if (!vsbar || !vsbar->isVisible()) {
+        // No vertical scrollbar. No need to scroll.
+        return;
+    }
+
+    QRect rect = p_edit->cursorRect();
+    int height = p_edit->rect().height();
+    QScrollBar *sbar = p_edit->horizontalScrollBar();
+    if (sbar && sbar->isVisible()) {
+        height -= sbar->height();
+    }
+
+    switch (p_dest) {
+    case 0:
+    {
+        // Top.
+        while (rect.y() > 0 && vsbar->value() < vsbar->maximum()) {
+            vsbar->setValue(vsbar->value() + vsbar->singleStep());
+            rect = p_edit->cursorRect();
+        }
+
+        break;
+    }
+
+    case 1:
+    {
+        // Center.
+        height = qMax(height / 2, 1);
+        if (rect.y() > height) {
+            while (rect.y() > height && vsbar->value() < vsbar->maximum()) {
+                vsbar->setValue(vsbar->value() + vsbar->singleStep());
+                rect = p_edit->cursorRect();
+            }
+        } else if (rect.y() < height) {
+            while (rect.y() < height && vsbar->value() > vsbar->minimum()) {
+                vsbar->setValue(vsbar->value() - vsbar->singleStep());
+                rect = p_edit->cursorRect();
+            }
+        }
+
+        break;
+    }
+
+    case 2:
+        // Bottom.
+        while (rect.y() < height && vsbar->value() > vsbar->minimum()) {
+            vsbar->setValue(vsbar->value() - vsbar->singleStep());
+            rect = p_edit->cursorRect();
+        }
+
+        break;
+
+    default:
+        break;
+    }
+
+    p_edit->ensureCursorVisible();
+}
+
 bool VEditUtils::isListBlock(const QTextBlock &p_block, int *p_seq)
 {
     QString text = p_block.text();

+ 9 - 0
src/utils/veditutils.h

@@ -6,6 +6,7 @@
 
 class QTextDocument;
 class QTextEdit;
+class QPlainTextEdit;
 
 // Utils for text edit.
 class VEditUtils
@@ -113,6 +114,14 @@ public:
                                   int p_blockNum,
                                   int p_dest);
 
+    // Scroll block @p_blockNum into the visual window.
+    // @p_dest is the position of the window: 0 for top, 1 for center, 2 for bottom.
+    // @p_blockNum is based on 0.
+    // Will set the cursor to the block.
+    static void scrollBlockInPage(QPlainTextEdit *p_edit,
+                                  int p_blockNum,
+                                  int p_dest);
+
     // Check if @p_block is a auto list block.
     // @p_seq will be the seq number of the ordered list, or -1.
     // Returns true if it is an auto list block.

+ 78 - 78
src/utils/vvim.cpp

@@ -9,7 +9,7 @@
 #include <QApplication>
 #include <QMimeData>
 #include "vconfigmanager.h"
-#include "vedit.h"
+#include "veditor.h"
 #include "utils/veditutils.h"
 #include "vconstants.h"
 
@@ -106,8 +106,8 @@ static QString keyToString(int p_key, int p_modifiers)
     }
 }
 
-VVim::VVim(VEdit *p_editor)
-    : QObject(p_editor), m_editor(p_editor),
+VVim::VVim(VEditor *p_editor)
+    : QObject(p_editor->getEditor()), m_editor(p_editor),
       m_editConfig(&p_editor->getConfig()), m_mode(VimMode::Invalid),
       m_resetPositionInBlock(true), m_regName(c_unnamedRegister),
       m_leaderKey(Key(Qt::Key_Space)), m_replayLeaderSequence(false),
@@ -119,7 +119,7 @@ VVim::VVim(VEdit *p_editor)
 
     initRegisters();
 
-    connect(m_editor, &VEdit::selectionChangedByMouse,
+    connect(m_editor->object(), &VEditorObject::selectionChangedByMouse,
             this, &VVim::selectionToVisualMode);
 }
 
@@ -472,13 +472,13 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
             // See if we need to cancel auto indent.
             bool cancelAutoIndent = false;
             if (p_autoIndentPos && *p_autoIndentPos > -1) {
-                QTextCursor cursor = m_editor->textCursor();
+                QTextCursor cursor = m_editor->textCursorW();
                 cancelAutoIndent = VEditUtils::needToCancelAutoIndent(*p_autoIndentPos, cursor);
 
                 if (cancelAutoIndent) {
                     autoIndentPos = -1;
                     VEditUtils::deleteIndentAndListMark(cursor);
-                    m_editor->setTextCursor(cursor);
+                    m_editor->setTextCursorW(cursor);
                 }
             }
 
@@ -501,7 +501,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
             QChar reg = keyToRegisterName(keyInfo);
             if (!reg.isNull()) {
                 // Insert register content.
-                m_editor->insertPlainText(getRegister(reg).read());
+                m_editor->insertPlainTextW(getRegister(reg).read());
             }
 
             goto clear_accept;
@@ -565,7 +565,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
         // Expecting a mark name to create a mark.
         if (keyInfo.isAlphabet() && modifiers == Qt::NoModifier) {
             m_keys.clear();
-            m_marks.setMark(keyToChar(key, modifiers), m_editor->textCursor());
+            m_marks.setMark(keyToChar(key, modifiers), m_editor->textCursorW());
         }
 
         goto clear_accept;
@@ -804,7 +804,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
                 setMode(VimMode::Insert, false);
             }
         } else if (modifiers == Qt::ShiftModifier) {
-            QTextCursor cursor = m_editor->textCursor();
+            QTextCursor cursor = m_editor->textCursorW();
             if (m_mode == VimMode::Normal) {
                 // Insert at the first non-space character.
                 VEditUtils::moveCursorFirstNonSpaceCharacter(cursor, QTextCursor::MoveAnchor);
@@ -815,7 +815,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
                                     1);
             }
 
-            m_editor->setTextCursor(cursor);
+            m_editor->setTextCursorW(cursor);
             setMode(VimMode::Insert);
         } else if (isControlModifier(modifiers)) {
             // Ctrl+I, jump to next location.
@@ -856,29 +856,29 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
             // Enter Insert mode.
             // Move cursor back one character.
             if (m_mode == VimMode::Normal) {
-                QTextCursor cursor = m_editor->textCursor();
+                QTextCursor cursor = m_editor->textCursorW();
                 V_ASSERT(!cursor.hasSelection());
 
                 if (!cursor.atBlockEnd()) {
                     cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 1);
-                    m_editor->setTextCursor(cursor);
+                    m_editor->setTextCursorW(cursor);
                 }
 
                 setMode(VimMode::Insert);
             }
         } else if (modifiers == Qt::ShiftModifier) {
             // Insert at the end of line.
-            QTextCursor cursor = m_editor->textCursor();
+            QTextCursor cursor = m_editor->textCursorW();
             if (m_mode == VimMode::Normal) {
                 cursor.movePosition(QTextCursor::EndOfBlock,
                                     QTextCursor::MoveAnchor,
                                     1);
-                m_editor->setTextCursor(cursor);
+                m_editor->setTextCursorW(cursor);
             } else if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) {
                 if (!cursor.atBlockEnd()) {
                     cursor.clearSelection();
                     cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 1);
-                    m_editor->setTextCursor(cursor);
+                    m_editor->setTextCursorW(cursor);
                 }
             }
 
@@ -894,7 +894,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
             // Insert a new block under/above current block and enter insert mode.
             bool insertAbove = modifiers == Qt::ShiftModifier;
             if (m_mode == VimMode::Normal) {
-                QTextCursor cursor = m_editor->textCursor();
+                QTextCursor cursor = m_editor->textCursorW();
                 cursor.beginEditBlock();
                 cursor.movePosition(insertAbove ? QTextCursor::StartOfBlock
                                                 : QTextCursor::EndOfBlock,
@@ -919,7 +919,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
                 }
 
                 cursor.endEditBlock();
-                m_editor->setTextCursor(cursor);
+                m_editor->setTextCursorW(cursor);
 
                 if (textInserted) {
                     autoIndentPos = cursor.position();
@@ -1104,7 +1104,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
         } else if (m_keys.isEmpty() && !hasActionToken()) {
             if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) {
                 // u/U for tolower and toupper selected text.
-                QTextCursor cursor = m_editor->textCursor();
+                QTextCursor cursor = m_editor->textCursorW();
                 cursor.beginEditBlock();
                 // Different from Vim:
                 // If there is no selection in Visual mode, we do nothing.
@@ -1116,7 +1116,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
 
                 convertCaseOfSelectedText(cursor, toLower);
                 cursor.endEditBlock();
-                m_editor->setTextCursor(cursor);
+                m_editor->setTextCursorW(cursor);
 
                 setMode(VimMode::Normal);
                 break;
@@ -1143,7 +1143,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
             } else if (checkPendingKey(Key(Qt::Key_G))) {
                 // gu/gU, ToLower/ToUpper action.
                 if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) {
-                    QTextCursor cursor = m_editor->textCursor();
+                    QTextCursor cursor = m_editor->textCursorW();
                     cursor.beginEditBlock();
                     // Different from Vim:
                     // If there is no selection in Visual mode, we do nothing.
@@ -1155,7 +1155,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
 
                     convertCaseOfSelectedText(cursor, toLower);
                     cursor.endEditBlock();
-                    m_editor->setTextCursor(cursor);
+                    m_editor->setTextCursorW(cursor);
                     setMode(VimMode::Normal);
                     break;
                 }
@@ -1338,7 +1338,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
         // Clear selection and enter normal mode.
         bool ret = clearSelection();
         if (!ret && checkMode(VimMode::Normal)) {
-            emit m_editor->requestCloseFindReplaceDialog();
+            emit m_editor->object()->requestCloseFindReplaceDialog();
         }
 
         setMode(VimMode::Normal);
@@ -1367,9 +1367,9 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
             setMode(mode);
 
             if (m_mode == VimMode::VisualLine) {
-                QTextCursor cursor = m_editor->textCursor();
+                QTextCursor cursor = m_editor->textCursorW();
                 expandSelectionToWholeLines(cursor);
-                m_editor->setTextCursor(cursor);
+                m_editor->setTextCursorW(cursor);
             }
         }
 
@@ -1681,8 +1681,8 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
             } else {
                 // The first >/<, an Action.
                 if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) {
-                    QTextCursor cursor = m_editor->textCursor();
-                    VEditUtils::indentSelectedBlocks(m_editor->document(),
+                    QTextCursor cursor = m_editor->textCursorW();
+                    VEditUtils::indentSelectedBlocks(m_editor->documentW(),
                                                      cursor,
                                                      m_editConfig->m_tabSpaces,
                                                      !unindent);
@@ -1857,7 +1857,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
                     // xx%, jump to a certain line (percentage of the documents).
                     // Change the repeat from percentage to line number.
                     Token *token = getRepeatToken();
-                    int bn = percentageToBlockNumber(m_editor->document(), token->m_repeat);
+                    int bn = percentageToBlockNumber(m_editor->documentW(), token->m_repeat);
                     if (bn == -1) {
                         break;
                     } else {
@@ -2148,7 +2148,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
 
 clear_accept:
     resetState();
-    m_editor->makeBlockVisible(m_editor->textCursor().block());
+    m_editor->makeBlockVisible(m_editor->textCursorW().block());
 
 accept:
     ret = true;
@@ -2353,7 +2353,7 @@ void VVim::processMoveAction(QList<Token> &p_tokens)
         return;
     }
 
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     if (m_resetPositionInBlock) {
         positionInBlock = cursor.positionInBlock();
     }
@@ -2389,7 +2389,7 @@ void VVim::processMoveAction(QList<Token> &p_tokens)
             expandSelectionToWholeLines(cursor);
         }
 
-        m_editor->setTextCursor(cursor);
+        m_editor->setTextCursorW(cursor);
     }
 }
 
@@ -3383,8 +3383,8 @@ void VVim::processDeleteAction(QList<Token> &p_tokens)
         return;
     }
 
-    QTextCursor cursor = m_editor->textCursor();
-    QTextDocument *doc = m_editor->document();
+    QTextCursor cursor = m_editor->textCursorW();
+    QTextDocument *doc = m_editor->documentW();
     bool hasMoved = false;
     QTextCursor::MoveMode moveMode = QTextCursor::KeepAnchor;
 
@@ -3598,7 +3598,7 @@ void VVim::processDeleteAction(QList<Token> &p_tokens)
 
 exit:
     if (hasMoved) {
-        m_editor->setTextCursor(cursor);
+        m_editor->setTextCursorW(cursor);
     }
 }
 
@@ -3616,8 +3616,8 @@ void VVim::processCopyAction(QList<Token> &p_tokens)
         return;
     }
 
-    QTextCursor cursor = m_editor->textCursor();
-    QTextDocument *doc = m_editor->document();
+    QTextCursor cursor = m_editor->textCursorW();
+    QTextDocument *doc = m_editor->documentW();
     int oriPos = cursor.position();
     bool changed = false;
     QTextCursor::MoveMode moveMode = QTextCursor::KeepAnchor;
@@ -3810,7 +3810,7 @@ void VVim::processCopyAction(QList<Token> &p_tokens)
 
 exit:
     if (changed) {
-        m_editor->setTextCursor(cursor);
+        m_editor->setTextCursorW(cursor);
     }
 }
 
@@ -3853,7 +3853,7 @@ void VVim::processPasteAction(QList<Token> &p_tokens, bool p_pasteBefore)
     bool changed = false;
     int nrBlock = 0;
     int restorePos = -1;
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     cursor.beginEditBlock();
 
     // Different from Vim:
@@ -3942,7 +3942,7 @@ void VVim::processPasteAction(QList<Token> &p_tokens, bool p_pasteBefore)
     cursor.endEditBlock();
 
     if (changed) {
-        m_editor->setTextCursor(cursor);
+        m_editor->setTextCursorW(cursor);
     }
 
     qDebug() << "text pasted" << text;
@@ -3962,8 +3962,8 @@ void VVim::processChangeAction(QList<Token> &p_tokens)
         return;
     }
 
-    QTextCursor cursor = m_editor->textCursor();
-    QTextDocument *doc = m_editor->document();
+    QTextCursor cursor = m_editor->textCursorW();
+    QTextDocument *doc = m_editor->documentW();
     bool hasMoved = false;
     QTextCursor::MoveMode moveMode = QTextCursor::KeepAnchor;
 
@@ -4226,7 +4226,7 @@ void VVim::processChangeAction(QList<Token> &p_tokens)
             int pos = cursor.selectionStart();
             bool allDeleted = false;
             if (pos == 0) {
-                QTextBlock block = m_editor->document()->lastBlock();
+                QTextBlock block = m_editor->documentW()->lastBlock();
                 if (block.position() + block.length() - 1 == cursor.selectionEnd()) {
                     allDeleted = true;
                 }
@@ -4243,7 +4243,7 @@ void VVim::processChangeAction(QList<Token> &p_tokens)
 
 exit:
     if (hasMoved) {
-        m_editor->setTextCursor(cursor);
+        m_editor->setTextCursorW(cursor);
     }
 
     setMode(VimMode::Insert);
@@ -4263,8 +4263,8 @@ void VVim::processIndentAction(QList<Token> &p_tokens, bool p_isIndent)
         return;
     }
 
-    QTextCursor cursor = m_editor->textCursor();
-    QTextDocument *doc = m_editor->document();
+    QTextCursor cursor = m_editor->textCursorW();
+    QTextDocument *doc = m_editor->documentW();
 
     if (to.isRange()) {
         bool changed = selectRange(cursor, doc, to.m_range, repeat);
@@ -4402,8 +4402,8 @@ void VVim::processToLowerAction(QList<Token> &p_tokens, bool p_toLower)
         return;
     }
 
-    QTextCursor cursor = m_editor->textCursor();
-    QTextDocument *doc = m_editor->document();
+    QTextCursor cursor = m_editor->textCursorW();
+    QTextDocument *doc = m_editor->documentW();
     bool changed = false;
     QTextCursor::MoveMode moveMode = QTextCursor::KeepAnchor;
     int oriPos = cursor.position();
@@ -4500,7 +4500,7 @@ void VVim::processToLowerAction(QList<Token> &p_tokens, bool p_toLower)
 
 exit:
     if (changed) {
-        m_editor->setTextCursor(cursor);
+        m_editor->setTextCursorW(cursor);
     }
 }
 
@@ -4517,10 +4517,10 @@ void VVim::processUndoAction(QList<Token> &p_tokens)
         repeat = to.m_repeat;
     }
 
-    QTextDocument *doc = m_editor->document();
+    QTextDocument *doc = m_editor->documentW();
     int i = 0;
     for (i = 0; i < repeat && doc->isUndoAvailable(); ++i) {
-        m_editor->undo();
+        m_editor->undoW();
     }
 
     message(tr("Undo %1 %2").arg(i).arg(i > 1 ? tr("changes") : tr("change")));
@@ -4539,10 +4539,10 @@ void VVim::processRedoAction(QList<Token> &p_tokens)
         repeat = to.m_repeat;
     }
 
-    QTextDocument *doc = m_editor->document();
+    QTextDocument *doc = m_editor->documentW();
     int i = 0;
     for (i = 0; i < repeat && doc->isRedoAvailable(); ++i) {
-        m_editor->redo();
+        m_editor->redoW();
     }
 
     message(tr("Redo %1 %2").arg(i).arg(i > 1 ? tr("changes") : tr("change")));
@@ -4550,7 +4550,7 @@ void VVim::processRedoAction(QList<Token> &p_tokens)
 
 void VVim::processRedrawLineAction(QList<Token> &p_tokens, int p_dest)
 {
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     int repeat = cursor.block().blockNumber();
     if (!p_tokens.isEmpty()) {
         Token to = p_tokens.takeFirst();
@@ -4562,7 +4562,7 @@ void VVim::processRedrawLineAction(QList<Token> &p_tokens, int p_dest)
         repeat = to.m_repeat - 1;
     }
 
-    VEditUtils::scrollBlockInPage(m_editor, repeat, p_dest);
+    m_editor->scrollBlockInPage(repeat, p_dest);
 }
 
 void VVim::processJumpLocationAction(QList<Token> &p_tokens, bool p_next)
@@ -4578,7 +4578,7 @@ void VVim::processJumpLocationAction(QList<Token> &p_tokens, bool p_next)
         repeat = to.m_repeat;
     }
 
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     Location loc;
     if (p_next) {
         while (m_locations.hasNext() && repeat > 0) {
@@ -4593,7 +4593,7 @@ void VVim::processJumpLocationAction(QList<Token> &p_tokens, bool p_next)
     }
 
     if (loc.isValid()) {
-        QTextDocument *doc = m_editor->document();
+        QTextDocument *doc = m_editor->documentW();
         if (loc.m_blockNumber >= doc->blockCount()) {
             message(tr("Mark has invalid line number"));
             return;
@@ -4607,11 +4607,11 @@ void VVim::processJumpLocationAction(QList<Token> &p_tokens, bool p_next)
 
         if (!m_editor->isBlockVisible(block)) {
             // Scroll the block to the center of screen.
-            VEditUtils::scrollBlockInPage(m_editor, block.blockNumber(), 1);
+            m_editor->scrollBlockInPage(block.blockNumber(), 1);
         }
 
         cursor.setPosition(block.position() + pib);
-        m_editor->setTextCursor(cursor);
+        m_editor->setTextCursorW(cursor);
     }
 }
 
@@ -4643,7 +4643,7 @@ void VVim::processReplaceAction(QList<Token> &p_tokens)
     // If repeat is greater than the number of left characters in current line,
     // do nothing.
     // In visual mode, repeat is ignored.
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     cursor.beginEditBlock();
     if (checkMode(VimMode::Normal)) {
         // Select the characters to be replaced.
@@ -4659,7 +4659,7 @@ void VVim::processReplaceAction(QList<Token> &p_tokens)
     cursor.endEditBlock();
 
     if (changed) {
-        m_editor->setTextCursor(cursor);
+        m_editor->setTextCursorW(cursor);
         setMode(VimMode::Normal);
     }
 }
@@ -4683,7 +4683,7 @@ void VVim::processReverseCaseAction(QList<Token> &p_tokens)
     // If repeat is greater than the number of left characters in current line,
     // just change the actual number of left characters.
     // In visual mode, repeat is ignored and reverse the selected text.
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     cursor.beginEditBlock();
     if (checkMode(VimMode::Normal)) {
         // Select the characters to be replaced.
@@ -4699,7 +4699,7 @@ void VVim::processReverseCaseAction(QList<Token> &p_tokens)
     cursor.endEditBlock();
 
     if (changed) {
-        m_editor->setTextCursor(cursor);
+        m_editor->setTextCursorW(cursor);
         setMode(VimMode::Normal);
     }
 }
@@ -4726,12 +4726,12 @@ void VVim::processJoinAction(QList<Token> &p_tokens, bool p_modifySpaces)
     // In visual mode, repeat is ignored and join the highlighted lines.
     int firstBlock = -1;
     int blockCount = repeat;
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     cursor.beginEditBlock();
     if (checkMode(VimMode::Normal)) {
         firstBlock = cursor.block().blockNumber();
     } else {
-        QTextDocument *doc = m_editor->document();
+        QTextDocument *doc = m_editor->documentW();
         firstBlock = doc->findBlock(cursor.selectionStart()).blockNumber();
         int lastBlock = doc->findBlock(cursor.selectionEnd()).blockNumber();
         blockCount = lastBlock - firstBlock + 1;
@@ -4741,17 +4741,17 @@ void VVim::processJoinAction(QList<Token> &p_tokens, bool p_modifySpaces)
     cursor.endEditBlock();
 
     if (changed) {
-        m_editor->setTextCursor(cursor);
+        m_editor->setTextCursorW(cursor);
         setMode(VimMode::Normal);
     }
 }
 
 bool VVim::clearSelection()
 {
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     if (cursor.hasSelection()) {
         cursor.clearSelection();
-        m_editor->setTextCursor(cursor);
+        m_editor->setTextCursorW(cursor);
         return true;
     }
 
@@ -4760,8 +4760,8 @@ bool VVim::clearSelection()
 
 int VVim::blockCountOfPageStep() const
 {
-    int lineCount = m_editor->document()->blockCount();
-    QScrollBar *bar = m_editor->verticalScrollBar();
+    int lineCount = m_editor->documentW()->blockCount();
+    QScrollBar *bar = m_editor->verticalScrollBarW();
     int steps = (bar->maximum() - bar->minimum() + bar->pageStep());
     int pageLineCount = lineCount * (bar->pageStep() * 1.0 / steps);
     return pageLineCount;
@@ -4781,7 +4781,7 @@ void VVim::selectionToVisualMode(bool p_hasText)
 
 void VVim::expandSelectionToWholeLines(QTextCursor &p_cursor)
 {
-    QTextDocument *doc = m_editor->document();
+    QTextDocument *doc = m_editor->documentW();
     int curPos = p_cursor.position();
     int anchorPos = p_cursor.anchor();
     QTextBlock curBlock = doc->findBlock(curPos);
@@ -5031,12 +5031,12 @@ void VVim::deleteSelectedText(QTextCursor &p_cursor, bool p_clearEmptyBlock)
 
 void VVim::copySelectedText(bool p_addNewLine)
 {
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     if (cursor.hasSelection()) {
         cursor.beginEditBlock();
         copySelectedText(cursor, p_addNewLine);
         cursor.endEditBlock();
-        m_editor->setTextCursor(cursor);
+        m_editor->setTextCursorW(cursor);
     }
 }
 
@@ -5318,15 +5318,15 @@ bool VVim::executeCommand(const QString &p_cmd)
     }else if (p_cmd.size() == 1) {
         if (p_cmd == "w") {
             // :w, save current file.
-            emit m_editor->saveNote();
+            emit m_editor->object()->saveNote();
             msg = tr("Note has been saved");
         } else if (p_cmd == "q") {
             // :q, quit edit mode.
-            emit m_editor->discardAndRead();
+            emit m_editor->object()->discardAndRead();
             msg = tr("Quit");
         } else if (p_cmd == "x") {
             // :x, save if there is any change and quit edit mode.
-            emit m_editor->saveAndRead();
+            emit m_editor->object()->saveAndRead();
             msg = tr("Quit with note having been saved");
         } else {
             validCommand = false;
@@ -5335,11 +5335,11 @@ bool VVim::executeCommand(const QString &p_cmd)
         if (p_cmd == "wq") {
             // :wq, save change and quit edit mode.
             // We treat it same as :x.
-            emit m_editor->saveAndRead();
+            emit m_editor->object()->saveAndRead();
             msg = tr("Quit with note having been saved");
         } else if (p_cmd == "q!") {
             // :q!, discard change and quit edit mode.
-            emit m_editor->discardAndRead();
+            emit m_editor->object()->discardAndRead();
             msg = tr("Quit");
         } else {
             validCommand = false;
@@ -5437,7 +5437,7 @@ bool VVim::processLeaderSequence(const Key &p_key)
         clearSearchHighlight();
     } else if (p_key == Key(Qt::Key_W)) {
         // <leader>w, save note
-        emit m_editor->saveNote();
+        emit m_editor->object()->saveNote();
         message(tr("Note has been saved"));
     } else {
         validSequence = false;
@@ -5624,7 +5624,7 @@ void VVim::processTitleJump(const QList<Token> &p_tokens, bool p_forward, int p_
         return;
     }
 
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     if (m_editor->jumpTitle(p_forward, p_relativeLevel, repeat)) {
         // Record current location.
         m_locations.addLocation(cursor);

+ 3 - 3
src/utils/vvim.h

@@ -8,7 +8,7 @@
 #include <QDebug>
 #include "vutils.h"
 
-class VEdit;
+class VEditor;
 class QKeyEvent;
 class VEditConfig;
 class QKeyEvent;
@@ -26,7 +26,7 @@ class VVim : public QObject
 {
     Q_OBJECT
 public:
-    explicit VVim(VEdit *p_editor);
+    explicit VVim(VEditor *p_editor);
 
     // Struct for a location.
     struct Location
@@ -801,7 +801,7 @@ private:
     Register &getRegister(QChar p_regName) const;
     void setRegister(QChar p_regName, const QString &p_val);
 
-    VEdit *m_editor;
+    VEditor *m_editor;
     const VEditConfig *m_editConfig;
     VimMode m_mode;
 

+ 0 - 8
src/vconstants.h

@@ -95,14 +95,6 @@ enum HighlightBlockState
     Comment
 };
 
-enum class LineNumberType
-{
-    None = 0,
-    Absolute,
-    Relative,
-    CodeBlock
-};
-
 // Pages to open on start up.
 enum class StartupPageType
 {

+ 0 - 38
src/vedit.cpp

@@ -18,44 +18,6 @@ extern VNote *g_vnote;
 
 extern VMetaWordManager *g_mwMgr;
 
-void VEditConfig::init(const QFontMetrics &p_metric,
-                       bool p_enableHeadingSequence)
-{
-    update(p_metric);
-
-    // Init configs that do not support update later.
-    m_enableVimMode = g_config->getEnableVimMode();
-
-    if (g_config->getLineDistanceHeight() <= 0) {
-        m_lineDistanceHeight = 0;
-    } else {
-        m_lineDistanceHeight = g_config->getLineDistanceHeight() * VUtils::calculateScaleFactor();
-    }
-
-    m_highlightWholeBlock = m_enableVimMode;
-
-    m_enableHeadingSequence = p_enableHeadingSequence;
-}
-
-void VEditConfig::update(const QFontMetrics &p_metric)
-{
-    if (g_config->getTabStopWidth() > 0) {
-        m_tabStopWidth = g_config->getTabStopWidth() * p_metric.width(' ');
-    } else {
-        m_tabStopWidth = 0;
-    }
-
-    m_expandTab = g_config->getIsExpandTab();
-
-    if (m_expandTab && (g_config->getTabStopWidth() > 0)) {
-        m_tabSpaces = QString(g_config->getTabStopWidth(), ' ');
-    } else {
-        m_tabSpaces = "\t";
-    }
-
-    m_cursorLineBg = QColor(g_config->getEditorCurrentLineBg());
-}
-
 VEdit::VEdit(VFile *p_file, QWidget *p_parent)
     : QTextEdit(p_parent), m_file(p_file),
       m_editOps(NULL), m_enableInputMethod(true)

+ 3 - 50
src/vedit.h

@@ -11,6 +11,8 @@
 #include <QFontMetrics>
 #include "vconstants.h"
 #include "vnotefile.h"
+#include "veditconfig.h"
+#include "veditor.h"
 
 class VEditOperations;
 class QLabel;
@@ -21,55 +23,6 @@ class QResizeEvent;
 class QSize;
 class QWidget;
 
-enum class SelectionId {
-    CurrentLine = 0,
-    SelectedWord,
-    SearchedKeyword,
-    SearchedKeywordUnderCursor,
-    IncrementalSearchedKeyword,
-    TrailingSapce,
-    MaxSelection
-};
-
-class VEditConfig {
-public:
-    VEditConfig() : m_tabStopWidth(0),
-                    m_tabSpaces("\t"),
-                    m_enableVimMode(false),
-                    m_highlightWholeBlock(false),
-                    m_lineDistanceHeight(0),
-                    m_enableHeadingSequence(false)
-    {}
-
-    void init(const QFontMetrics &p_metric,
-              bool p_enableHeadingSequence);
-
-    // Only update those configs which could be updated online.
-    void update(const QFontMetrics &p_metric);
-
-    // Width in pixels.
-    int m_tabStopWidth;
-
-    bool m_expandTab;
-
-    // The literal string for Tab. It is spaces if Tab is expanded.
-    QString m_tabSpaces;
-
-    bool m_enableVimMode;
-
-    // The background color of cursor line.
-    QColor m_cursorLineBg;
-
-    // Whether highlight a visual line or a whole block.
-    bool m_highlightWholeBlock;
-
-    // Line distance height in pixels.
-    int m_lineDistanceHeight;
-
-    // Whether enable auto heading sequence.
-    bool m_enableHeadingSequence;
-};
-
 class LineNumberArea;
 
 class VEdit : public QTextEdit
@@ -98,7 +51,7 @@ public:
     // User has enter the content to search, but does not enter the "find" button yet.
     bool peekText(const QString &p_text, uint p_options, bool p_forward = true);
 
-    // If @p_cursor is not now, set the position of @p_cursor instead of current
+    // If @p_cursor is not null, set the position of @p_cursor instead of current
     // cursor.
     bool findText(const QString &p_text, uint p_options, bool p_forward,
                   QTextCursor *p_cursor = NULL,

+ 44 - 0
src/veditconfig.cpp

@@ -0,0 +1,44 @@
+#include "veditconfig.h"
+
+#include "vconfigmanager.h"
+#include "utils/vutils.h"
+
+extern VConfigManager *g_config;
+
+void VEditConfig::init(const QFontMetrics &p_metric,
+                       bool p_enableHeadingSequence)
+{
+    update(p_metric);
+
+    // Init configs that do not support update later.
+    m_enableVimMode = g_config->getEnableVimMode();
+
+    if (g_config->getLineDistanceHeight() <= 0) {
+        m_lineDistanceHeight = 0;
+    } else {
+        m_lineDistanceHeight = g_config->getLineDistanceHeight() * VUtils::calculateScaleFactor();
+    }
+
+    m_highlightWholeBlock = m_enableVimMode;
+
+    m_enableHeadingSequence = p_enableHeadingSequence;
+}
+
+void VEditConfig::update(const QFontMetrics &p_metric)
+{
+    if (g_config->getTabStopWidth() > 0) {
+        m_tabStopWidth = g_config->getTabStopWidth() * p_metric.width(' ');
+    } else {
+        m_tabStopWidth = 0;
+    }
+
+    m_expandTab = g_config->getIsExpandTab();
+
+    if (m_expandTab && (g_config->getTabStopWidth() > 0)) {
+        m_tabSpaces = QString(g_config->getTabStopWidth(), ' ');
+    } else {
+        m_tabSpaces = "\t";
+    }
+
+    m_cursorLineBg = QColor(g_config->getEditorCurrentLineBg());
+}

+ 48 - 0
src/veditconfig.h

@@ -0,0 +1,48 @@
+#ifndef VEDITCONFIG_H
+#define VEDITCONFIG_H
+
+#include <QFontMetrics>
+#include <QString>
+#include <QColor>
+
+
+class VEditConfig {
+public:
+    VEditConfig() : m_tabStopWidth(0),
+                    m_tabSpaces("\t"),
+                    m_enableVimMode(false),
+                    m_highlightWholeBlock(false),
+                    m_lineDistanceHeight(0),
+                    m_enableHeadingSequence(false)
+    {}
+
+    void init(const QFontMetrics &p_metric,
+              bool p_enableHeadingSequence);
+
+    // Only update those configs which could be updated online.
+    void update(const QFontMetrics &p_metric);
+
+    // Width in pixels.
+    int m_tabStopWidth;
+
+    bool m_expandTab;
+
+    // The literal string for Tab. It is spaces if Tab is expanded.
+    QString m_tabSpaces;
+
+    bool m_enableVimMode;
+
+    // The background color of cursor line.
+    QColor m_cursorLineBg;
+
+    // Whether highlight a visual line or a whole block.
+    bool m_highlightWholeBlock;
+
+    // Line distance height in pixels.
+    int m_lineDistanceHeight;
+
+    // Whether enable auto heading sequence.
+    bool m_enableHeadingSequence;
+};
+
+#endif // VEDITCONFIG_H

+ 9 - 6
src/veditoperations.cpp

@@ -1,18 +1,21 @@
 #include <QTextCursor>
 #include <QTextDocument>
 #include <QFontMetrics>
-#include "vedit.h"
+#include "veditor.h"
 #include "veditoperations.h"
 #include "vconfigmanager.h"
 #include "utils/vutils.h"
 
 extern VConfigManager *g_config;
 
-VEditOperations::VEditOperations(VEdit *p_editor, VFile *p_file)
-    : QObject(p_editor), m_editor(p_editor), m_file(p_file),
-      m_editConfig(&p_editor->getConfig()), m_vim(NULL)
+VEditOperations::VEditOperations(VEditor *p_editor, VFile *p_file)
+    : QObject(p_editor->getEditor()),
+      m_editor(p_editor),
+      m_file(p_file),
+      m_editConfig(&p_editor->getConfig()),
+      m_vim(NULL)
 {
-    connect(m_editor, &VEdit::configUpdated,
+    connect(m_editor->object(), &VEditorObject::configUpdated,
             this, &VEditOperations::handleEditConfigUpdated);
 
     if (m_editConfig->m_enableVimMode) {
@@ -29,7 +32,7 @@ VEditOperations::VEditOperations(VEdit *p_editor, VFile *p_file)
 
 void VEditOperations::insertTextAtCurPos(const QString &p_text)
 {
-    m_editor->insertPlainText(p_text);
+    m_editor->insertPlainTextW(p_text);
 }
 
 VEditOperations::~VEditOperations()

+ 3 - 3
src/veditoperations.h

@@ -8,7 +8,7 @@
 #include "vfile.h"
 #include "utils/vvim.h"
 
-class VEdit;
+class VEditor;
 class VEditConfig;
 class QMimeData;
 class QKeyEvent;
@@ -17,7 +17,7 @@ class VEditOperations: public QObject
 {
     Q_OBJECT
 public:
-    VEditOperations(VEdit *p_editor, VFile *p_file);
+    VEditOperations(VEditor *p_editor, VFile *p_file);
 
     virtual ~VEditOperations();
 
@@ -64,7 +64,7 @@ private:
 protected:
     void insertTextAtCurPos(const QString &p_text);
 
-    VEdit *m_editor;
+    VEditor *m_editor;
     QPointer<VFile> m_file;
     VEditConfig *m_editConfig;
     VVim *m_vim;

+ 917 - 0
src/veditor.cpp

@@ -0,0 +1,917 @@
+#include "veditor.h"
+
+#include <QtWidgets>
+#include <QTextDocument>
+
+#include "vconfigmanager.h"
+#include "utils/vutils.h"
+#include "utils/veditutils.h"
+#include "veditoperations.h"
+#include "dialog/vinsertlinkdialog.h"
+#include "utils/vmetawordmanager.h"
+
+extern VConfigManager *g_config;
+
+extern VMetaWordManager *g_mwMgr;
+
+VEditor::VEditor(VFile *p_file, QWidget *p_editor)
+    : m_editor(p_editor),
+      m_object(new VEditorObject(this, p_editor)),
+      m_file(p_file),
+      m_editOps(nullptr),
+      m_document(nullptr),
+      m_enableInputMethod(true)
+{
+}
+
+VEditor::~VEditor()
+{
+    if (m_file && m_document) {
+        QObject::disconnect(m_document, &QTextDocument::modificationChanged,
+                            (VFile *)m_file, &VFile::setModified);
+    }
+}
+
+void VEditor::init()
+{
+    const int labelTimerInterval = 500;
+    const int extraSelectionHighlightTimer = 500;
+    const int labelSize = 64;
+
+    m_document = documentW();
+
+    m_selectedWordColor = QColor(g_config->getEditorSelectedWordBg());
+    m_searchedWordColor = QColor(g_config->getEditorSearchedWordBg());
+    m_searchedWordCursorColor = QColor(g_config->getEditorSearchedWordCursorBg());
+    m_incrementalSearchedWordColor = QColor(g_config->getEditorIncrementalSearchedWordBg());
+    m_trailingSpaceColor = QColor(g_config->getEditorTrailingSpaceBg());
+
+    QPixmap wrapPixmap(":/resources/icons/search_wrap.svg");
+    m_wrapLabel = new QLabel(m_editor);
+    m_wrapLabel->setPixmap(wrapPixmap.scaled(labelSize, labelSize));
+    m_wrapLabel->hide();
+    m_labelTimer = new QTimer(m_editor);
+    m_labelTimer->setSingleShot(true);
+    m_labelTimer->setInterval(labelTimerInterval);
+    QObject::connect(m_labelTimer, &QTimer::timeout,
+                     m_object, &VEditorObject::labelTimerTimeout);
+
+    m_highlightTimer = new QTimer(m_editor);
+    m_highlightTimer->setSingleShot(true);
+    m_highlightTimer->setInterval(extraSelectionHighlightTimer);
+    QObject::connect(m_highlightTimer, &QTimer::timeout,
+                     m_object, &VEditorObject::doHighlightExtraSelections);
+
+    m_extraSelections.resize((int)SelectionId::MaxSelection);
+
+    QObject::connect(m_document, &QTextDocument::modificationChanged,
+                     (VFile *)m_file, &VFile::setModified);
+
+    updateFontAndPalette();
+
+    m_config.init(QFontMetrics(m_editor->font()), false);
+    updateEditConfig();
+}
+
+void VEditor::labelTimerTimeout()
+{
+    m_wrapLabel->hide();
+}
+
+void VEditor::doHighlightExtraSelections()
+{
+    int nrExtra = m_extraSelections.size();
+    Q_ASSERT(nrExtra == (int)SelectionId::MaxSelection);
+    QList<QTextEdit::ExtraSelection> extraSelects;
+    for (int i = 0; i < nrExtra; ++i) {
+        extraSelects.append(m_extraSelections[i]);
+    }
+
+    setExtraSelectionsW(extraSelects);
+}
+
+void VEditor::updateEditConfig()
+{
+    m_config.update(QFontMetrics(m_editor->font()));
+
+    if (m_config.m_tabStopWidth > 0) {
+        setTabStopWidthW(m_config.m_tabStopWidth);
+    }
+
+    emit m_object->configUpdated();
+}
+
+void VEditor::highlightOnCursorPositionChanged()
+{
+    static QTextCursor lastCursor;
+
+    QTextCursor cursor = textCursorW();
+    if (lastCursor.isNull() || cursor.blockNumber() != lastCursor.blockNumber()) {
+        highlightCurrentLine();
+        highlightTrailingSpace();
+    } else {
+        // Judge whether we have trailing space at current line.
+        QString text = cursor.block().text();
+        if (text.rbegin()->isSpace()) {
+            highlightTrailingSpace();
+        }
+
+        // Handle word-wrap in one block.
+        // Highlight current line if in different visual line.
+        if ((lastCursor.positionInBlock() - lastCursor.columnNumber()) !=
+            (cursor.positionInBlock() - cursor.columnNumber())) {
+            highlightCurrentLine();
+        }
+    }
+
+    lastCursor = cursor;
+}
+
+void VEditor::highlightCurrentLine()
+{
+    QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::CurrentLine];
+    if (g_config->getHighlightCursorLine()) {
+        // Need to highlight current line.
+        selects.clear();
+
+        // A long block maybe splited into multiple visual lines.
+        QTextEdit::ExtraSelection select;
+        select.format.setBackground(m_config.m_cursorLineBg);
+        select.format.setProperty(QTextFormat::FullWidthSelection, true);
+
+        QTextCursor cursor = textCursorW();
+        if (m_config.m_highlightWholeBlock) {
+            cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor, 1);
+            QTextBlock block = cursor.block();
+            int blockEnd = block.position() + block.length();
+            int pos = -1;
+            while (cursor.position() < blockEnd && pos != cursor.position()) {
+                QTextEdit::ExtraSelection newSelect = select;
+                newSelect.cursor = cursor;
+                selects.append(newSelect);
+
+                pos = cursor.position();
+                cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, 1);
+            }
+        } else {
+            cursor.clearSelection();
+            select.cursor = cursor;
+            selects.append(select);
+        }
+    } else {
+        // Need to clear current line highlight.
+        if (selects.isEmpty()) {
+            return;
+        }
+
+        selects.clear();
+    }
+
+    highlightExtraSelections(true);
+}
+
+// Do not highlight trailing spaces with current cursor right behind.
+static void trailingSpaceFilter(VEditor *p_editor, QList<QTextEdit::ExtraSelection> &p_result)
+{
+    QTextCursor cursor = p_editor->textCursorW();
+    if (!cursor.atBlockEnd()) {
+        return;
+    }
+
+    int cursorPos = cursor.position();
+    for (auto it = p_result.begin(); it != p_result.end(); ++it) {
+        if (it->cursor.selectionEnd() == cursorPos) {
+            p_result.erase(it);
+
+            // There will be only one.
+            return;
+        }
+    }
+}
+
+void VEditor::highlightTrailingSpace()
+{
+    if (!g_config->getEnableTrailingSpaceHighlight()) {
+        QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::TrailingSapce];
+        if (!selects.isEmpty()) {
+            selects.clear();
+            highlightExtraSelections(true);
+        }
+        return;
+    }
+
+    QTextCharFormat format;
+    format.setBackground(m_trailingSpaceColor);
+    QString text("\\s+$");
+    highlightTextAll(text,
+                     FindOption::RegularExpression,
+                     SelectionId::TrailingSapce,
+                     format,
+                     trailingSpaceFilter);
+}
+
+void VEditor::highlightExtraSelections(bool p_now)
+{
+    m_highlightTimer->stop();
+    if (p_now) {
+        doHighlightExtraSelections();
+    } else {
+        m_highlightTimer->start();
+    }
+}
+
+void VEditor::highlightTextAll(const QString &p_text,
+                               uint p_options,
+                               SelectionId p_id,
+                               QTextCharFormat p_format,
+                               void (*p_filter)(VEditor *,
+                                                QList<QTextEdit::ExtraSelection> &))
+{
+    QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)p_id];
+    if (!p_text.isEmpty()) {
+        selects.clear();
+
+        QList<QTextCursor> occurs = findTextAll(p_text, p_options);
+        for (int i = 0; i < occurs.size(); ++i) {
+            QTextEdit::ExtraSelection select;
+            select.format = p_format;
+            select.cursor = occurs[i];
+            selects.append(select);
+        }
+    } else {
+        if (selects.isEmpty()) {
+            return;
+        }
+        selects.clear();
+    }
+
+    if (p_filter) {
+        p_filter(this, selects);
+    }
+
+    highlightExtraSelections();
+}
+
+QList<QTextCursor> VEditor::findTextAll(const QString &p_text, uint p_options)
+{
+    QList<QTextCursor> results;
+    if (p_text.isEmpty()) {
+        return results;
+    }
+
+    // Options
+    QTextDocument::FindFlags findFlags;
+    bool caseSensitive = false;
+    if (p_options & FindOption::CaseSensitive) {
+        findFlags |= QTextDocument::FindCaseSensitively;
+        caseSensitive = true;
+    }
+
+    if (p_options & FindOption::WholeWordOnly) {
+        findFlags |= QTextDocument::FindWholeWords;
+    }
+
+    // Use regular expression
+    bool useRegExp = false;
+    QRegExp exp;
+    if (p_options & FindOption::RegularExpression) {
+        useRegExp = true;
+        exp = QRegExp(p_text,
+                      caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
+    }
+
+    int startPos = 0;
+    QTextCursor cursor;
+    while (true) {
+        if (useRegExp) {
+            cursor = m_document->find(exp, startPos, findFlags);
+        } else {
+            cursor = m_document->find(p_text, startPos, findFlags);
+        }
+
+        if (cursor.isNull()) {
+            break;
+        } else {
+            results.append(cursor);
+            startPos = cursor.selectionEnd();
+        }
+    }
+
+    return results;
+}
+
+void VEditor::highlightSelectedWord()
+{
+    QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::SelectedWord];
+    if (!g_config->getHighlightSelectedWord()) {
+        if (!selects.isEmpty()) {
+            selects.clear();
+            highlightExtraSelections(true);
+        }
+
+        return;
+    }
+
+    QString text = textCursorW().selectedText().trimmed();
+    if (text.isEmpty() || wordInSearchedSelection(text)) {
+        selects.clear();
+        highlightExtraSelections(true);
+        return;
+    }
+
+    QTextCharFormat format;
+    format.setBackground(m_selectedWordColor);
+    highlightTextAll(text,
+                     FindOption::CaseSensitive,
+                     SelectionId::SelectedWord,
+                     format);
+}
+
+bool VEditor::wordInSearchedSelection(const QString &p_text)
+{
+    QString text = p_text.trimmed();
+    QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::SearchedKeyword];
+    for (int i = 0; i < selects.size(); ++i) {
+        QString searchedWord = selects[i].cursor.selectedText();
+        if (text == searchedWord.trimmed()) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+bool VEditor::isModified() const
+{
+    Q_ASSERT(m_file ? (m_file->isModified() == m_document->isModified())
+                    : true);
+    return m_document->isModified();
+}
+
+void VEditor::setModified(bool p_modified)
+{
+    m_document->setModified(p_modified);
+    if (m_file) {
+        m_file->setModified(p_modified);
+    }
+}
+
+void VEditor::insertImage()
+{
+    if (m_editOps) {
+        m_editOps->insertImage();
+    }
+}
+
+void VEditor::insertLink()
+{
+    if (!m_editOps) {
+        return;
+    }
+
+    QString text;
+    QString linkText, linkUrl;
+    QTextCursor cursor = textCursorW();
+    if (cursor.hasSelection()) {
+        text = VEditUtils::selectedText(cursor).trimmed();
+        // Only pure space is accepted.
+        QRegExp reg("[\\S ]*");
+        if (reg.exactMatch(text)) {
+            QUrl url = QUrl::fromUserInput(text,
+                                           m_file->fetchBasePath());
+            QRegExp urlReg("[\\.\\\\/]");
+            if (url.isValid()
+                && text.contains(urlReg)) {
+                // Url.
+                linkUrl = text;
+            } else {
+                // Text.
+                linkText = text;
+            }
+        }
+    }
+
+    VInsertLinkDialog dialog(QObject::tr("Insert Link"),
+                             "",
+                             "",
+                             linkText,
+                             linkUrl,
+                             m_editor);
+    if (dialog.exec() == QDialog::Accepted) {
+        linkText = dialog.getLinkText();
+        linkUrl = dialog.getLinkUrl();
+        Q_ASSERT(!linkText.isEmpty() && !linkUrl.isEmpty());
+
+        m_editOps->insertLink(linkText, linkUrl);
+    }
+}
+
+bool VEditor::peekText(const QString &p_text, uint p_options, bool p_forward)
+{
+    if (p_text.isEmpty()) {
+        makeBlockVisible(m_document->findBlock(textCursorW().selectionStart()));
+        highlightIncrementalSearchedWord(QTextCursor());
+        return false;
+    }
+
+    bool wrapped = false;
+    QTextCursor retCursor;
+    bool found = findTextHelper(p_text,
+                                p_options,
+                                p_forward,
+                                p_forward ? textCursorW().position() + 1
+                                          : textCursorW().position(),
+                                wrapped,
+                                retCursor);
+    if (found) {
+        makeBlockVisible(m_document->findBlock(retCursor.selectionStart()));
+        highlightIncrementalSearchedWord(retCursor);
+    }
+
+    return found;
+}
+
+bool VEditor::findText(const QString &p_text,
+                       uint p_options,
+                       bool p_forward,
+                       QTextCursor *p_cursor,
+                       QTextCursor::MoveMode p_moveMode)
+{
+    clearIncrementalSearchedWordHighlight();
+
+    if (p_text.isEmpty()) {
+        clearSearchedWordHighlight();
+        return false;
+    }
+
+    QTextCursor cursor = textCursorW();
+    bool wrapped = false;
+    QTextCursor retCursor;
+    int matches = 0;
+    int start = p_forward ? cursor.position() + 1 : cursor.position();
+    if (p_cursor) {
+        start = p_forward ? p_cursor->position() + 1 : p_cursor->position();
+    }
+
+    bool found = findTextHelper(p_text, p_options, p_forward, start,
+                                wrapped, retCursor);
+    if (found) {
+        Q_ASSERT(!retCursor.isNull());
+        if (wrapped) {
+            showWrapLabel();
+        }
+
+        if (p_cursor) {
+            p_cursor->setPosition(retCursor.selectionStart(), p_moveMode);
+        } else {
+            cursor.setPosition(retCursor.selectionStart(), p_moveMode);
+            setTextCursorW(cursor);
+        }
+
+        highlightSearchedWord(p_text, p_options);
+        highlightSearchedWordUnderCursor(retCursor);
+        matches = m_extraSelections[(int)SelectionId::SearchedKeyword].size();
+    } else {
+        clearSearchedWordHighlight();
+    }
+
+    if (matches == 0) {
+        emit m_object->statusMessage(QObject::tr("Found no match"));
+    } else {
+        emit m_object->statusMessage(QObject::tr("Found %1 %2").arg(matches)
+                                                               .arg(matches > 1 ? QObject::tr("matches")
+                                                                                : QObject::tr("match")));
+    }
+
+    return found;
+}
+
+void VEditor::highlightIncrementalSearchedWord(const QTextCursor &p_cursor)
+{
+    QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::IncrementalSearchedKeyword];
+    if (!g_config->getHighlightSearchedWord() || !p_cursor.hasSelection()) {
+        if (!selects.isEmpty()) {
+            selects.clear();
+            highlightExtraSelections(true);
+        }
+
+        return;
+    }
+
+    selects.clear();
+    QTextEdit::ExtraSelection select;
+    select.format.setBackground(m_incrementalSearchedWordColor);
+    select.cursor = p_cursor;
+    selects.append(select);
+
+    highlightExtraSelections(true);
+}
+
+// Use QPlainTextEdit::find() instead of QTextDocument::find() because the later has
+// bugs in searching backward.
+bool VEditor::findTextHelper(const QString &p_text,
+                             uint p_options,
+                             bool p_forward,
+                             int p_start,
+                             bool &p_wrapped,
+                             QTextCursor &p_cursor)
+{
+    p_wrapped = false;
+    bool found = false;
+
+    // Options
+    QTextDocument::FindFlags findFlags;
+    bool caseSensitive = false;
+    if (p_options & FindOption::CaseSensitive) {
+        findFlags |= QTextDocument::FindCaseSensitively;
+        caseSensitive = true;
+    }
+
+    if (p_options & FindOption::WholeWordOnly) {
+        findFlags |= QTextDocument::FindWholeWords;
+    }
+
+    if (!p_forward) {
+        findFlags |= QTextDocument::FindBackward;
+    }
+
+    // Use regular expression
+    bool useRegExp = false;
+    QRegExp exp;
+    if (p_options & FindOption::RegularExpression) {
+        useRegExp = true;
+        exp = QRegExp(p_text,
+                      caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
+    }
+
+    // Store current state of the cursor.
+    QTextCursor cursor = textCursorW();
+    if (cursor.position() != p_start) {
+        if (p_start < 0) {
+            p_start = 0;
+        } else if (p_start > m_document->characterCount()) {
+            p_start = m_document->characterCount();
+        }
+
+        QTextCursor startCursor = cursor;
+        startCursor.setPosition(p_start);
+        setTextCursorW(startCursor);
+    }
+
+    while (!found) {
+        if (useRegExp) {
+            found = findW(exp, findFlags);
+        } else {
+            found = findW(p_text, findFlags);
+        }
+
+        if (p_wrapped) {
+            break;
+        }
+
+        if (!found) {
+            // Wrap to the other end of the document to search again.
+            p_wrapped = true;
+            QTextCursor wrapCursor = textCursorW();
+            if (p_forward) {
+                wrapCursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
+            } else {
+                wrapCursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
+            }
+
+            setTextCursorW(wrapCursor);
+        }
+    }
+
+    if (found) {
+        p_cursor = textCursorW();
+    }
+
+    // Restore the original cursor.
+    setTextCursorW(cursor);
+
+    return found;
+}
+
+void VEditor::clearIncrementalSearchedWordHighlight(bool p_now)
+{
+    QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::IncrementalSearchedKeyword];
+    if (selects.isEmpty()) {
+        return;
+    }
+
+    selects.clear();
+    highlightExtraSelections(p_now);
+}
+
+void VEditor::clearSearchedWordHighlight()
+{
+    clearIncrementalSearchedWordHighlight(false);
+    clearSearchedWordUnderCursorHighlight(false);
+
+    QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::SearchedKeyword];
+    if (selects.isEmpty()) {
+        return;
+    }
+
+    selects.clear();
+    highlightExtraSelections(true);
+}
+
+void VEditor::clearSearchedWordUnderCursorHighlight(bool p_now)
+{
+    QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::SearchedKeywordUnderCursor];
+    if (selects.isEmpty()) {
+        return;
+    }
+
+    selects.clear();
+    highlightExtraSelections(p_now);
+}
+
+void VEditor::showWrapLabel()
+{
+    int labelW = m_wrapLabel->width();
+    int labelH = m_wrapLabel->height();
+    int x = (m_editor->width() - labelW) / 2;
+    int y = (m_editor->height() - labelH) / 2;
+    if (x < 0) {
+        x = 0;
+    }
+
+    if (y < 0) {
+        y = 0;
+    }
+
+    m_wrapLabel->move(x, y);
+    m_wrapLabel->show();
+    m_labelTimer->stop();
+    m_labelTimer->start();
+}
+
+void VEditor::highlightSearchedWord(const QString &p_text, uint p_options)
+{
+    QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::SearchedKeyword];
+    if (!g_config->getHighlightSearchedWord() || p_text.isEmpty()) {
+        if (!selects.isEmpty()) {
+            selects.clear();
+            highlightExtraSelections(true);
+        }
+
+        return;
+    }
+
+    QTextCharFormat format;
+    format.setBackground(m_searchedWordColor);
+    highlightTextAll(p_text, p_options, SelectionId::SearchedKeyword, format);
+}
+
+void VEditor::highlightSearchedWordUnderCursor(const QTextCursor &p_cursor)
+{
+    QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::SearchedKeywordUnderCursor];
+    if (!p_cursor.hasSelection()) {
+        if (!selects.isEmpty()) {
+            selects.clear();
+            highlightExtraSelections(true);
+        }
+
+        return;
+    }
+
+    selects.clear();
+    QTextEdit::ExtraSelection select;
+    select.format.setBackground(m_searchedWordCursorColor);
+    select.cursor = p_cursor;
+    selects.append(select);
+
+    highlightExtraSelections(true);
+}
+
+void VEditor::replaceText(const QString &p_text,
+                          uint p_options,
+                          const QString &p_replaceText,
+                          bool p_findNext)
+{
+    QTextCursor cursor = textCursorW();
+    bool wrapped = false;
+    QTextCursor retCursor;
+    bool found = findTextHelper(p_text,
+                                p_options, true,
+                                cursor.position(),
+                                wrapped,
+                                retCursor);
+    if (found) {
+        if (retCursor.selectionStart() == cursor.position()) {
+            // Matched.
+            retCursor.beginEditBlock();
+            retCursor.insertText(p_replaceText);
+            retCursor.endEditBlock();
+            setTextCursorW(retCursor);
+        }
+
+        if (p_findNext) {
+            findText(p_text, p_options, true);
+        }
+    }
+}
+
+void VEditor::replaceTextAll(const QString &p_text,
+                             uint p_options,
+                             const QString &p_replaceText)
+{
+    // Replace from the start to the end and restore the cursor.
+    QTextCursor cursor = textCursorW();
+    int nrReplaces = 0;
+    QTextCursor tmpCursor = cursor;
+    tmpCursor.setPosition(0);
+    setTextCursorW(tmpCursor);
+    int start = tmpCursor.position();
+    while (true) {
+        bool wrapped = false;
+        QTextCursor retCursor;
+        bool found = findTextHelper(p_text,
+                                    p_options,
+                                    true,
+                                    start,
+                                    wrapped,
+                                    retCursor);
+        if (!found) {
+            break;
+        } else {
+            if (wrapped) {
+                // Wrap back.
+                break;
+            }
+
+            nrReplaces++;
+            retCursor.beginEditBlock();
+            retCursor.insertText(p_replaceText);
+            retCursor.endEditBlock();
+            setTextCursorW(retCursor);
+            start = retCursor.position();
+        }
+    }
+
+    // Restore cursor position.
+    cursor.clearSelection();
+    setTextCursorW(cursor);
+    qDebug() << "replace all" << nrReplaces << "occurences";
+
+    emit m_object->statusMessage(QObject::tr("Replace %1 %2").arg(nrReplaces)
+                                                             .arg(nrReplaces > 1 ? QObject::tr("occurences")
+                                                                                 : QObject::tr("occurence")));
+}
+
+void VEditor::evaluateMagicWords()
+{
+    QString text;
+    QTextCursor cursor = textCursorW();
+    if (!cursor.hasSelection()) {
+        // Get the WORD in current cursor.
+        int start, end;
+        VEditUtils::findCurrentWORD(cursor, start, end);
+
+        if (start == end) {
+            return;
+        } else {
+            cursor.setPosition(start);
+            cursor.setPosition(end, QTextCursor::KeepAnchor);
+        }
+    }
+
+    text = VEditUtils::selectedText(cursor);
+    Q_ASSERT(!text.isEmpty());
+    QString evaText = g_mwMgr->evaluate(text);
+    if (text != evaText) {
+        qDebug() << "evaluateMagicWords" << text << evaText;
+
+        cursor.insertText(evaText);
+
+        if (m_editOps) {
+            m_editOps->setVimMode(VimMode::Insert);
+        }
+
+        setTextCursorW(cursor);
+    }
+}
+
+void VEditor::setReadOnlyAndHighlightCurrentLine(bool p_readonly)
+{
+    setReadOnlyW(p_readonly);
+    highlightCurrentLine();
+}
+
+bool VEditor::handleMousePressEvent(QMouseEvent *p_event)
+{
+    if (p_event->button() == Qt::LeftButton
+        && p_event->modifiers() == Qt::ControlModifier
+        && !textCursorW().hasSelection()) {
+        m_oriMouseX = p_event->x();
+        m_oriMouseY = p_event->y();
+        m_readyToScroll = true;
+        m_mouseMoveScrolled = false;
+        p_event->accept();
+        return true;
+    }
+
+    m_readyToScroll = false;
+    m_mouseMoveScrolled = false;
+
+    return false;
+}
+
+bool VEditor::handleMouseReleaseEvent(QMouseEvent *p_event)
+{
+    if (m_mouseMoveScrolled || m_readyToScroll) {
+        viewportW()->setCursor(Qt::IBeamCursor);
+        m_readyToScroll = false;
+        m_mouseMoveScrolled = false;
+        p_event->accept();
+        return true;
+    }
+
+    m_readyToScroll = false;
+    m_mouseMoveScrolled = false;
+
+    return false;
+}
+
+bool VEditor::handleMouseMoveEvent(QMouseEvent *p_event)
+{
+    const int threshold = 5;
+
+    if (m_readyToScroll) {
+        int deltaX = p_event->x() - m_oriMouseX;
+        int deltaY = p_event->y() - m_oriMouseY;
+
+        if (qAbs(deltaX) >= threshold || qAbs(deltaY) >= threshold) {
+            m_oriMouseX = p_event->x();
+            m_oriMouseY = p_event->y();
+
+            if (!m_mouseMoveScrolled) {
+                m_mouseMoveScrolled = true;
+                viewportW()->setCursor(Qt::SizeAllCursor);
+            }
+
+            QScrollBar *verBar = verticalScrollBarW();
+            QScrollBar *horBar = horizontalScrollBarW();
+            if (verBar->isVisible()) {
+                verBar->setValue(verBar->value() - deltaY);
+            }
+
+            if (horBar->isVisible()) {
+                horBar->setValue(horBar->value() - deltaX);
+            }
+        }
+
+        p_event->accept();
+        return true;
+    }
+
+    return false;
+}
+
+void VEditor::requestUpdateVimStatus()
+{
+    if (m_editOps) {
+        m_editOps->requestUpdateVimStatus();
+    } else {
+        emit m_object->vimStatusUpdated(NULL);
+    }
+}
+
+bool VEditor::handleInputMethodQuery(Qt::InputMethodQuery p_query,
+                                     QVariant &p_var) const
+{
+    if (p_query == Qt::ImEnabled) {
+        p_var = m_enableInputMethod;
+        return true;
+    }
+
+    return false;
+}
+
+void VEditor::setInputMethodEnabled(bool p_enabled)
+{
+    if (m_enableInputMethod != p_enabled) {
+        m_enableInputMethod = p_enabled;
+
+        QInputMethod *im = QGuiApplication::inputMethod();
+        im->reset();
+
+        // Ask input method to query current state, which will call inputMethodQuery().
+        im->update(Qt::ImEnabled);
+    }
+}
+
+void VEditor::decorateText(TextDecoration p_decoration)
+{
+    if (m_editOps) {
+        m_editOps->decorateText(p_decoration);
+    }
+}
+
+void VEditor::updateConfig()
+{
+    updateEditConfig();
+}

+ 372 - 0
src/veditor.h

@@ -0,0 +1,372 @@
+#ifndef VEDITOR_H
+#define VEDITOR_H
+
+#include <QPointer>
+#include <QVector>
+#include <QList>
+#include <QTextEdit>
+#include <QColor>
+
+#include "veditconfig.h"
+#include "vfile.h"
+
+class QWidget;
+class VEditorObject;
+class VEditOperations;
+class QTimer;
+class QLabel;
+class VVim;
+
+
+enum class SelectionId {
+    CurrentLine = 0,
+    SelectedWord,
+    SearchedKeyword,
+    SearchedKeywordUnderCursor,
+    IncrementalSearchedKeyword,
+    TrailingSapce,
+    MaxSelection
+};
+
+
+// Abstract class for an edit.
+// Should inherit this class as well as QPlainTextEdit or QTextEdit.
+// Will replace VEdit eventually.
+class VEditor
+{
+public:
+    explicit VEditor(VFile *p_file, QWidget *p_editor);
+
+    virtual ~VEditor();
+
+    void highlightCurrentLine();
+
+    virtual void beginEdit() = 0;
+
+    virtual void endEdit() = 0;
+
+    // Save buffer content to VFile.
+    virtual void saveFile() = 0;
+
+    virtual void reloadFile() = 0;
+
+    virtual bool scrollToBlock(int p_blockNumber) = 0;
+
+    bool isModified() const;
+
+    void setModified(bool p_modified);
+
+    // User requests to insert an image.
+    void insertImage();
+
+    // User requests to insert a link.
+    void insertLink();
+
+    // Used for incremental search.
+    // User has enter the content to search, but does not enter the "find" button yet.
+    bool peekText(const QString &p_text, uint p_options, bool p_forward = true);
+
+    // If @p_cursor is not null, set the position of @p_cursor instead of current
+    // cursor.
+    bool findText(const QString &p_text,
+                  uint p_options,
+                  bool p_forward,
+                  QTextCursor *p_cursor = nullptr,
+                  QTextCursor::MoveMode p_moveMode = QTextCursor::MoveAnchor);
+
+    void replaceText(const QString &p_text,
+                     uint p_options,
+                     const QString &p_replaceText,
+                     bool p_findNext);
+
+    void replaceTextAll(const QString &p_text,
+                        uint p_options,
+                        const QString &p_replaceText);
+
+    // Scroll the content to make @p_block visible.
+    // If the @p_block is too long to hold in one page, just let it occupy the
+    // whole page.
+    // Will not change current cursor.
+    virtual void makeBlockVisible(const QTextBlock &p_block) = 0;
+
+    // Clear IncrementalSearchedKeyword highlight.
+    void clearIncrementalSearchedWordHighlight(bool p_now = true);
+
+    // Clear SearchedKeyword highlight.
+    void clearSearchedWordHighlight();
+
+    // Clear SearchedKeywordUnderCursor Highlight.
+    void clearSearchedWordUnderCursorHighlight(bool p_now = true);
+
+    // Evaluate selected text or cursor word as magic words.
+    void evaluateMagicWords();
+
+    VFile *getFile() const;
+
+    VEditConfig &getConfig();
+
+    // Request to update Vim status.
+    void requestUpdateVimStatus();
+
+    // Jump to a title.
+    // @p_forward: jump forward or backward.
+    // @p_relativeLevel: 0 for the same level as current header;
+    //                   negative value for upper level;
+    //                   positive value is ignored.
+    // Returns true if the jump succeeded.
+    virtual bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) = 0;
+
+    void setInputMethodEnabled(bool p_enabled);
+
+    // Insert decoration markers or decorate selected text.
+    void decorateText(TextDecoration p_decoration);
+
+    virtual bool isBlockVisible(const QTextBlock &p_block) = 0;
+
+    VEditorObject *object() const;
+
+    QWidget *getEditor() const;
+
+    // Scroll block @p_blockNum into the visual window.
+    // @p_dest is the position of the window: 0 for top, 1 for center, 2 for bottom.
+    // @p_blockNum is based on 0.
+    // Will set the cursor to the block.
+    virtual void scrollBlockInPage(int p_blockNum, int p_dest) = 0;
+
+    // Update config according to global configurations.
+    virtual void updateConfig();
+
+// Wrapper functions for QPlainTextEdit/QTextEdit.
+// Ends with W to distinguish it from the original interfaces.
+public:
+    virtual void setExtraSelectionsW(const QList<QTextEdit::ExtraSelection> &p_selections) = 0;
+
+    virtual QTextDocument *documentW() const = 0;
+
+    virtual void setTabStopWidthW(int p_width) = 0;
+
+    virtual QTextCursor textCursorW() const = 0;
+
+    virtual void setTextCursorW(const QTextCursor &p_cursor) = 0;
+
+    virtual void moveCursorW(QTextCursor::MoveOperation p_operation,
+                             QTextCursor::MoveMode p_mode = QTextCursor::MoveAnchor) = 0;
+
+    virtual QScrollBar *verticalScrollBarW() const = 0;
+
+    virtual QScrollBar *horizontalScrollBarW() const = 0;
+
+    virtual bool findW(const QString &p_exp,
+                       QTextDocument::FindFlags p_options = QTextDocument::FindFlags()) = 0;
+
+    virtual bool findW(const QRegExp &p_exp,
+                       QTextDocument::FindFlags p_options = QTextDocument::FindFlags()) = 0;
+
+    virtual void setReadOnlyW(bool p_ro) = 0;
+
+    virtual QWidget *viewportW() const = 0;
+
+    virtual void insertPlainTextW(const QString &p_text) = 0;
+
+    virtual void undoW() = 0;
+
+    virtual void redoW() = 0;
+
+protected:
+    void init();
+
+    virtual void updateFontAndPalette() = 0;
+
+    // Update m_config according to VConfigManager.
+    void updateEditConfig();
+
+    // Do some highlight on cursor position changed.
+    void highlightOnCursorPositionChanged();
+
+    // Highlight selected text.
+    void highlightSelectedWord();
+
+    bool wordInSearchedSelection(const QString &p_text);
+
+    // Set read-only property and highlight current line.
+    void setReadOnlyAndHighlightCurrentLine(bool p_readonly);
+
+    // Handle the mouse press event of m_editor.
+    // Returns true if no further process is needed.
+    bool handleMousePressEvent(QMouseEvent *p_event);
+
+    bool handleMouseReleaseEvent(QMouseEvent *p_event);
+
+    bool handleMouseMoveEvent(QMouseEvent *p_event);
+
+    bool handleInputMethodQuery(Qt::InputMethodQuery p_query,
+                                QVariant &p_var) const;
+
+    QWidget *m_editor;
+
+    VEditorObject *m_object;
+
+    QPointer<VFile> m_file;
+
+    VEditOperations *m_editOps;
+
+    VEditConfig m_config;
+
+private:
+    friend class VEditorObject;
+
+    void highlightTrailingSpace();
+
+    // Trigger the timer to request highlight.
+    // If @p_now is true, stop the timer and highlight immediately.
+    void highlightExtraSelections(bool p_now = false);
+
+    // @p_fileter: a function to filter out highlight results.
+    void highlightTextAll(const QString &p_text,
+                          uint p_options,
+                          SelectionId p_id,
+                          QTextCharFormat p_format,
+                          void (*p_filter)(VEditor *,
+                                           QList<QTextEdit::ExtraSelection> &) = NULL);
+
+    // Find all the occurences of @p_text.
+    QList<QTextCursor> findTextAll(const QString &p_text, uint p_options);
+
+    // Highlight @p_cursor as the incremental searched keyword.
+    void highlightIncrementalSearchedWord(const QTextCursor &p_cursor);
+
+    // Find @p_text in the document starting from @p_start.
+    // Returns true if @p_text is found and set @p_cursor to indicate
+    // the position.
+    // Will NOT change current cursor.
+    bool findTextHelper(const QString &p_text,
+                        uint p_options,
+                        bool p_forward,
+                        int p_start,
+                        bool &p_wrapped,
+                        QTextCursor &p_cursor);
+
+    void showWrapLabel();
+
+    void highlightSearchedWord(const QString &p_text, uint p_options);
+
+    // Highlight @p_cursor as the searched keyword under cursor.
+    void highlightSearchedWordUnderCursor(const QTextCursor &p_cursor);
+
+    QLabel *m_wrapLabel;
+    QTimer *m_labelTimer;
+
+    QTextDocument *m_document;
+
+    // doHighlightExtraSelections() will highlight these selections.
+    // Selections are indexed by SelectionId.
+    QVector<QList<QTextEdit::ExtraSelection> > m_extraSelections;
+
+    QColor m_selectedWordColor;
+    QColor m_searchedWordColor;
+    QColor m_searchedWordCursorColor;
+    QColor m_incrementalSearchedWordColor;
+    QColor m_trailingSpaceColor;
+
+    // Timer for extra selections highlight.
+    QTimer *m_highlightTimer;
+
+    bool m_readyToScroll;
+    bool m_mouseMoveScrolled;
+    int m_oriMouseX;
+    int m_oriMouseY;
+
+    // Whether enable input method.
+    bool m_enableInputMethod;
+
+// Functions for private slots.
+private:
+    void labelTimerTimeout();
+
+    // Do the real work to highlight extra selections.
+    void doHighlightExtraSelections();
+};
+
+
+// Since one class could not inherit QObject multiple times, we use this class
+// for VEditor to signal/slot.
+class VEditorObject : public QObject
+{
+    Q_OBJECT
+public:
+    explicit VEditorObject(VEditor *p_editor, QObject *p_parent = nullptr)
+        : QObject(p_parent), m_editor(p_editor)
+    {
+    }
+
+signals:
+    // Emit when editor config has been updated.
+    void configUpdated();
+
+    // Emit when want to show message in status bar.
+    void statusMessage(const QString &p_msg);
+
+    // Request VEditTab to save and exit edit mode.
+    void saveAndRead();
+
+    // Request VEditTab to discard and exit edit mode.
+    void discardAndRead();
+
+    // Request VEditTab to edit current note.
+    void editNote();
+
+    // Request VEditTab to save this file.
+    void saveNote();
+
+    // Selection changed by mouse.
+    void selectionChangedByMouse(bool p_hasSelection);
+
+    // Emit when Vim status updated.
+    void vimStatusUpdated(const VVim *p_vim);
+
+    // Emit when all initialization is ready.
+    void ready();
+
+    // Request the edit tab to close find and replace dialog.
+    void requestCloseFindReplaceDialog();
+
+private slots:
+    // Timer for find-wrap label.
+    void labelTimerTimeout()
+    {
+        m_editor->labelTimerTimeout();
+    }
+
+    // Do the real work to highlight extra selections.
+    void doHighlightExtraSelections()
+    {
+        m_editor->doHighlightExtraSelections();
+    }
+
+private:
+    friend class VEditor;
+
+    VEditor *m_editor;
+};
+
+inline VFile *VEditor::getFile() const
+{
+    return m_file;
+}
+
+inline VEditConfig &VEditor::getConfig()
+{
+    return m_config;
+}
+
+inline VEditorObject *VEditor::object() const
+{
+    return m_object;
+}
+
+inline QWidget *VEditor::getEditor() const
+{
+    return m_editor;
+}
+
+#endif // VEDITOR_H

+ 2 - 2
src/vfilelist.cpp

@@ -10,7 +10,7 @@
 #include "utils/vutils.h"
 #include "vnotefile.h"
 #include "vconfigmanager.h"
-#include "vmdedit.h"
+#include "vmdeditor.h"
 #include "vmdtab.h"
 #include "dialog/vconfirmdeletiondialog.h"
 #include "dialog/vsortdialog.h"
@@ -376,7 +376,7 @@ void VFileList::newFile()
         if (contentInserted) {
             const VMdTab *tab = dynamic_cast<VMdTab *>(editArea->getCurrentTab());
             if (tab) {
-                VMdEdit *edit = dynamic_cast<VMdEdit *>(tab->getEditor());
+                VMdEditor *edit = tab->getEditor();
                 if (edit && edit->getFile() == file) {
                     QTextCursor cursor = edit->textCursor();
                     cursor.movePosition(QTextCursor::End);

+ 1 - 1
src/vimagepreviewer.h

@@ -84,7 +84,7 @@ private:
         int m_endPos;
         QString m_linkUrl;
 
-        // Whether it is a image block.
+        // Whether it is an image block.
         bool m_isBlock;
 
         // The previewed image ID if this link has been previewed.

+ 1 - 1
src/vimageresourcemanager.cpp

@@ -44,7 +44,7 @@ void VImageResourceManager::updateBlockInfos(const QVector<VBlockImageInfo> &p_b
 
     // Clear unused images.
     for (auto it = m_images.begin(); it != m_images.end();) {
-        if (!m_images.contains(it.key())) {
+        if (!usedImages.contains(it.key())) {
             // Remove the image.
             it = m_images.erase(it);
         } else {

+ 2 - 1
src/vlinenumberarea.h

@@ -13,7 +13,8 @@ enum class LineNumberType
     None = 0,
     Absolute,
     Relative,
-    CodeBlock
+    CodeBlock,
+    Invalid
 };
 
 

+ 3 - 0
src/vmainwindow.cpp

@@ -1630,6 +1630,7 @@ void VMainWindow::initEditorLineNumberMenu(QMenu *p_menu)
                 }
 
                 g_config->setEditorLineNumber(p_action->data().toInt());
+                emit editorConfigUpdated();
             });
 
     int lineNumberMode = g_config->getEditorLineNumber();
@@ -2280,6 +2281,8 @@ void VMainWindow::enableImagePreview(bool p_checked)
 void VMainWindow::enableImagePreviewConstraint(bool p_checked)
 {
     g_config->setEnablePreviewImageConstraint(p_checked);
+
+    emit editorConfigUpdated();
 }
 
 void VMainWindow::enableImageConstraint(bool p_checked)

+ 4 - 0
src/vmainwindow.h

@@ -86,6 +86,10 @@ public:
     // Prompt user for new notebook if there is no notebook.
     void promptNewNotebookIfEmpty();
 
+signals:
+    // Emit when editor related configurations were changed by user.
+    void editorConfigUpdated();
+
 private slots:
     void importNoteFromFile();
     void viewSettings();

+ 3 - 0
src/vmdedit.cpp

@@ -66,6 +66,8 @@ VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type,
                 }
             });
 
+    // Comment out these lines since we use VMdEditor to replace VMdEdit.
+    /*
     m_editOps = new VMdEditOperations(this, m_file);
 
     connect(m_editOps, &VEditOperations::statusMessage,
@@ -78,6 +80,7 @@ VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type,
 
     connect(QApplication::clipboard(), &QClipboard::changed,
             this, &VMdEdit::handleClipboardChanged);
+    */
 
     updateFontAndPalette();
 

+ 47 - 45
src/vmdeditoperations.cpp

@@ -16,10 +16,10 @@
 #include "dialog/vinsertimagedialog.h"
 #include "dialog/vselectdialog.h"
 #include "utils/vutils.h"
-#include "vedit.h"
+#include "veditor.h"
 #include "vdownloader.h"
 #include "vfile.h"
-#include "vmdedit.h"
+#include "vmdeditor.h"
 #include "vconfigmanager.h"
 #include "utils/vvim.h"
 #include "utils/veditutils.h"
@@ -28,7 +28,7 @@ extern VConfigManager *g_config;
 
 const QString VMdEditOperations::c_defaultImageTitle = "";
 
-VMdEditOperations::VMdEditOperations(VEdit *p_editor, VFile *p_file)
+VMdEditOperations::VMdEditOperations(VEditor *p_editor, VFile *p_file)
     : VEditOperations(p_editor, p_file), m_autoIndentPos(-1)
 {
 }
@@ -40,7 +40,9 @@ bool VMdEditOperations::insertImageFromMimeData(const QMimeData *source)
         return false;
     }
     VInsertImageDialog dialog(tr("Insert Image From Clipboard"),
-                              c_defaultImageTitle, "", (QWidget *)m_editor);
+                              c_defaultImageTitle,
+                              "",
+                              m_editor->getEditor());
     dialog.setBrowseable(false);
     dialog.setImage(image);
     if (dialog.exec() == QDialog::Accepted) {
@@ -78,7 +80,7 @@ void VMdEditOperations::insertImageFromQImage(const QString &title, const QStrin
                             errStr,
                             QMessageBox::Ok,
                             QMessageBox::Ok,
-                            (QWidget *)m_editor);
+                            m_editor->getEditor());
         return;
     }
 
@@ -87,7 +89,7 @@ void VMdEditOperations::insertImageFromQImage(const QString &title, const QStrin
 
     qDebug() << "insert image" << title << filePath;
 
-    VMdEdit *mdEditor = dynamic_cast<VMdEdit *>(m_editor);
+    VMdEditor *mdEditor = dynamic_cast<VMdEditor *>(m_editor);
     Q_ASSERT(mdEditor);
     mdEditor->imageInserted(filePath);
 }
@@ -118,7 +120,7 @@ void VMdEditOperations::insertImageFromPath(const QString &title, const QString
                             errStr,
                             QMessageBox::Ok,
                             QMessageBox::Ok,
-                            (QWidget *)m_editor);
+                            m_editor->getEditor());
         return;
     }
 
@@ -127,7 +129,7 @@ void VMdEditOperations::insertImageFromPath(const QString &title, const QString
 
     qDebug() << "insert image" << title << filePath;
 
-    VMdEdit *mdEditor = dynamic_cast<VMdEdit *>(m_editor);
+    VMdEditor *mdEditor = dynamic_cast<VMdEditor *>(m_editor);
     Q_ASSERT(mdEditor);
     mdEditor->imageInserted(filePath);
 }
@@ -156,7 +158,7 @@ bool VMdEditOperations::insertImageFromURL(const QUrl &imageUrl)
 
 
     VInsertImageDialog dialog(title, c_defaultImageTitle,
-                              imagePath, (QWidget *)m_editor);
+                              imagePath, m_editor->getEditor());
     dialog.setBrowseable(false, true);
     if (isLocal) {
         dialog.setImage(image);
@@ -186,7 +188,7 @@ bool VMdEditOperations::insertImageFromURL(const QUrl &imageUrl)
 bool VMdEditOperations::insertImage()
 {
     VInsertImageDialog dialog(tr("Insert Image From File"),
-                              c_defaultImageTitle, "", (QWidget *)m_editor);
+                              c_defaultImageTitle, "", m_editor->getEditor());
     if (dialog.exec() == QDialog::Accepted) {
         QString title = dialog.getImageTitleInput();
         QString imagePath = dialog.getPathInput();
@@ -393,10 +395,10 @@ bool VMdEditOperations::handleKeyBracketLeft(QKeyEvent *p_event)
     // 1. If there is any selection, clear it.
     // 2. Otherwise, ignore this event and let parent handles it.
     if (p_event->modifiers() == Qt::ControlModifier) {
-        QTextCursor cursor = m_editor->textCursor();
+        QTextCursor cursor = m_editor->textCursorW();
         if (cursor.hasSelection()) {
             cursor.clearSelection();
-            m_editor->setTextCursor(cursor);
+            m_editor->setTextCursorW(cursor);
             p_event->accept();
             return true;
         }
@@ -407,18 +409,18 @@ bool VMdEditOperations::handleKeyBracketLeft(QKeyEvent *p_event)
 
 bool VMdEditOperations::handleKeyTab(QKeyEvent *p_event)
 {
-    QTextDocument *doc = m_editor->document();
+    QTextDocument *doc = m_editor->documentW();
     QString text(m_editConfig->m_tabSpaces);
 
     if (p_event->modifiers() == Qt::NoModifier) {
-        QTextCursor cursor = m_editor->textCursor();
+        QTextCursor cursor = m_editor->textCursorW();
         if (cursor.hasSelection()) {
             m_autoIndentPos = -1;
             cursor.beginEditBlock();
             // Indent each selected line.
             VEditUtils::indentSelectedBlocks(doc, cursor, text, true);
             cursor.endEditBlock();
-            m_editor->setTextCursor(cursor);
+            m_editor->setTextCursorW(cursor);
         } else {
             // If it is a Tab key following auto list, increase the indent level.
             QTextBlock block = cursor.block();
@@ -433,7 +435,7 @@ bool VMdEditOperations::handleKeyTab(QKeyEvent *p_event)
                 }
                 blockCursor.endEditBlock();
                 // Change m_autoIndentPos to let it can be repeated.
-                m_autoIndentPos = m_editor->textCursor().position();
+                m_autoIndentPos = m_editor->textCursorW().position();
             } else {
                 // Just insert "tab".
                 insertTextAtCurPos(text);
@@ -454,8 +456,8 @@ bool VMdEditOperations::handleKeyBackTab(QKeyEvent *p_event)
         m_autoIndentPos = -1;
         return false;
     }
-    QTextDocument *doc = m_editor->document();
-    QTextCursor cursor = m_editor->textCursor();
+    QTextDocument *doc = m_editor->documentW();
+    QTextCursor cursor = m_editor->textCursorW();
     QTextBlock block = doc->findBlock(cursor.selectionStart());
     bool continueAutoIndent = false;
     int seq = -1;
@@ -474,7 +476,7 @@ bool VMdEditOperations::handleKeyBackTab(QKeyEvent *p_event)
     cursor.endEditBlock();
 
     if (continueAutoIndent) {
-        m_autoIndentPos = m_editor->textCursor().position();
+        m_autoIndentPos = m_editor->textCursorW().position();
     } else {
         m_autoIndentPos = -1;
     }
@@ -486,7 +488,7 @@ bool VMdEditOperations::handleKeyH(QKeyEvent *p_event)
 {
     if (p_event->modifiers() == Qt::ControlModifier) {
         // Ctrl+H, equal to backspace.
-        QTextCursor cursor = m_editor->textCursor();
+        QTextCursor cursor = m_editor->textCursorW();
         cursor.deletePreviousChar();
 
         p_event->accept();
@@ -499,7 +501,7 @@ bool VMdEditOperations::handleKeyU(QKeyEvent *p_event)
 {
     if (p_event->modifiers() == Qt::ControlModifier) {
         // Ctrl+U, delete till the start of line.
-        QTextCursor cursor = m_editor->textCursor();
+        QTextCursor cursor = m_editor->textCursorW();
         bool ret;
         if (cursor.atBlockStart()) {
             ret = cursor.movePosition(QTextCursor::PreviousWord, QTextCursor::KeepAnchor);
@@ -520,7 +522,7 @@ bool VMdEditOperations::handleKeyW(QKeyEvent *p_event)
 {
     if (p_event->modifiers() == Qt::ControlModifier) {
         // Ctrl+W, delete till the start of previous word.
-        QTextCursor cursor = m_editor->textCursor();
+        QTextCursor cursor = m_editor->textCursorW();
         if (cursor.hasSelection()) {
             cursor.removeSelectedText();
         } else {
@@ -539,10 +541,10 @@ bool VMdEditOperations::handleKeyEsc(QKeyEvent *p_event)
 {
     // 1. If there is any selection, clear it.
     // 2. Otherwise, ignore this event and let parent handles it.
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     if (cursor.hasSelection()) {
         cursor.clearSelection();
-        m_editor->setTextCursor(cursor);
+        m_editor->setTextCursorW(cursor);
         p_event->accept();
         return true;
     }
@@ -560,7 +562,7 @@ bool VMdEditOperations::handleKeyReturn(QKeyEvent *p_event)
         // Insert two spaces and a new line.
         m_autoIndentPos = -1;
 
-        QTextCursor cursor = m_editor->textCursor();
+        QTextCursor cursor = m_editor->textCursorW();
         cursor.beginEditBlock();
         cursor.removeSelectedText();
         cursor.insertText("  ");
@@ -575,11 +577,11 @@ bool VMdEditOperations::handleKeyReturn(QKeyEvent *p_event)
     if (m_autoIndentPos > -1) {
         // Cancel the auto indent/list if the pos is the same and cursor is at
         // the end of a block.
-        QTextCursor cursor = m_editor->textCursor();
+        QTextCursor cursor = m_editor->textCursorW();
         if (VEditUtils::needToCancelAutoIndent(m_autoIndentPos, cursor)) {
             m_autoIndentPos = -1;
             VEditUtils::deleteIndentAndListMark(cursor);
-            m_editor->setTextCursor(cursor);
+            m_editor->setTextCursorW(cursor);
             return true;
         }
     }
@@ -589,7 +591,7 @@ bool VMdEditOperations::handleKeyReturn(QKeyEvent *p_event)
     if (g_config->getAutoIndent()) {
         handled = true;
 
-        QTextCursor cursor = m_editor->textCursor();
+        QTextCursor cursor = m_editor->textCursorW();
         bool textInserted = false;
         cursor.beginEditBlock();
         cursor.removeSelectedText();
@@ -603,9 +605,9 @@ bool VMdEditOperations::handleKeyReturn(QKeyEvent *p_event)
         }
 
         cursor.endEditBlock();
-        m_editor->setTextCursor(cursor);
+        m_editor->setTextCursorW(cursor);
         if (textInserted) {
-            m_autoIndentPos = m_editor->textCursor().position();
+            m_autoIndentPos = m_editor->textCursorW().position();
         }
     }
 
@@ -648,8 +650,8 @@ void VMdEditOperations::changeListBlockSeqNumber(QTextBlock &p_block, int p_seq)
 
 bool VMdEditOperations::insertTitle(int p_level)
 {
-    QTextDocument *doc = m_editor->document();
-    QTextCursor cursor = m_editor->textCursor();
+    QTextDocument *doc = m_editor->documentW();
+    QTextCursor cursor = m_editor->textCursorW();
     int firstBlock = cursor.block().blockNumber();
     int lastBlock = firstBlock;
 
@@ -667,7 +669,7 @@ bool VMdEditOperations::insertTitle(int p_level)
     }
 
     cursor.endEditBlock();
-    m_editor->setTextCursor(cursor);
+    m_editor->setTextCursorW(cursor);
     return true;
 }
 
@@ -713,7 +715,7 @@ void VMdEditOperations::decorateText(TextDecoration p_decoration)
 
 void VMdEditOperations::decorateBold()
 {
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     cursor.beginEditBlock();
     if (cursor.hasSelection()) {
         // Insert ** around the selected text.
@@ -745,12 +747,12 @@ void VMdEditOperations::decorateBold()
     }
 
     cursor.endEditBlock();
-    m_editor->setTextCursor(cursor);
+    m_editor->setTextCursorW(cursor);
 }
 
 void VMdEditOperations::decorateItalic()
 {
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     cursor.beginEditBlock();
     if (cursor.hasSelection()) {
         // Insert * around the selected text.
@@ -782,12 +784,12 @@ void VMdEditOperations::decorateItalic()
     }
 
     cursor.endEditBlock();
-    m_editor->setTextCursor(cursor);
+    m_editor->setTextCursorW(cursor);
 }
 
 void VMdEditOperations::decorateInlineCode()
 {
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     cursor.beginEditBlock();
     if (cursor.hasSelection()) {
         // Insert ` around the selected text.
@@ -819,14 +821,14 @@ void VMdEditOperations::decorateInlineCode()
     }
 
     cursor.endEditBlock();
-    m_editor->setTextCursor(cursor);
+    m_editor->setTextCursorW(cursor);
 }
 
 void VMdEditOperations::decorateCodeBlock()
 {
     const QString marker("```");
 
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     cursor.beginEditBlock();
     if (cursor.hasSelection()) {
         // Insert ``` around the selected text.
@@ -899,12 +901,12 @@ void VMdEditOperations::decorateCodeBlock()
     }
 
     cursor.endEditBlock();
-    m_editor->setTextCursor(cursor);
+    m_editor->setTextCursorW(cursor);
 }
 
 void VMdEditOperations::decorateStrikethrough()
 {
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     cursor.beginEditBlock();
     if (cursor.hasSelection()) {
         // Insert ~~ around the selected text.
@@ -936,16 +938,16 @@ void VMdEditOperations::decorateStrikethrough()
     }
 
     cursor.endEditBlock();
-    m_editor->setTextCursor(cursor);
+    m_editor->setTextCursorW(cursor);
 }
 
 bool VMdEditOperations::insertLink(const QString &p_linkText,
                                    const QString &p_linkUrl)
 {
     QString link = QString("[%1](%2)").arg(p_linkText).arg(p_linkUrl);
-    QTextCursor cursor = m_editor->textCursor();
+    QTextCursor cursor = m_editor->textCursorW();
     cursor.insertText(link);
-    m_editor->setTextCursor(cursor);
+    m_editor->setTextCursorW(cursor);
 
     setVimMode(VimMode::Insert);
 

+ 1 - 1
src/vmdeditoperations.h

@@ -15,7 +15,7 @@ class VMdEditOperations : public VEditOperations
 {
     Q_OBJECT
 public:
-    VMdEditOperations(VEdit *p_editor, VFile *p_file);
+    VMdEditOperations(VEditor *p_editor, VFile *p_file);
 
     bool insertImageFromMimeData(const QMimeData *source) Q_DECL_OVERRIDE;
 

+ 862 - 0
src/vmdeditor.cpp

@@ -0,0 +1,862 @@
+#include "vmdeditor.h"
+
+#include <QtWidgets>
+#include <QMenu>
+#include <QDebug>
+
+#include "vdocument.h"
+#include "utils/veditutils.h"
+#include "vedittab.h"
+#include "hgmarkdownhighlighter.h"
+#include "vcodeblockhighlighthelper.h"
+#include "vmdeditoperations.h"
+#include "vtableofcontent.h"
+#include "utils/veditutils.h"
+#include "dialog/vselectdialog.h"
+#include "dialog/vconfirmdeletiondialog.h"
+#include "vtextblockdata.h"
+#include "vorphanfile.h"
+#include "vnotefile.h"
+#include "vpreviewmanager.h"
+
+extern VConfigManager *g_config;
+
+VMdEditor::VMdEditor(VFile *p_file,
+                     VDocument *p_doc,
+                     MarkdownConverterType p_type,
+                     QWidget *p_parent)
+    : VPlainTextEdit(p_parent),
+      VEditor(p_file, this),
+      m_mdHighlighter(NULL),
+      m_freshEdit(true)
+{
+    Q_ASSERT(p_file->getDocType() == DocType::Markdown);
+
+    VEditor::init();
+
+    // Hook functions from VEditor.
+    connect(this, &VPlainTextEdit::cursorPositionChanged,
+            this, [this]() {
+                highlightOnCursorPositionChanged();
+            });
+
+    connect(this, &VPlainTextEdit::selectionChanged,
+            this, [this]() {
+                highlightSelectedWord();
+            });
+    // End.
+
+    m_mdHighlighter = new HGMarkdownHighlighter(g_config->getMdHighlightingStyles(),
+                                                g_config->getCodeBlockStyles(),
+                                                g_config->getMarkdownHighlightInterval(),
+                                                document());
+
+    connect(m_mdHighlighter, &HGMarkdownHighlighter::headersUpdated,
+            this, &VMdEditor::updateHeaders);
+
+    // After highlight, the cursor may trun into non-visible. We should make it visible
+    // in this case.
+    connect(m_mdHighlighter, &HGMarkdownHighlighter::highlightCompleted,
+            this, [this]() {
+            makeBlockVisible(textCursor().block());
+    });
+
+    m_cbHighlighter = new VCodeBlockHighlightHelper(m_mdHighlighter,
+                                                    p_doc,
+                                                    p_type);
+
+    m_previewMgr = new VPreviewManager(this);
+    connect(m_mdHighlighter, &HGMarkdownHighlighter::imageLinksUpdated,
+            m_previewMgr, &VPreviewManager::imageLinksUpdated);
+    connect(m_previewMgr, &VPreviewManager::requestUpdateImageLinks,
+            m_mdHighlighter, &HGMarkdownHighlighter::updateHighlight);
+
+    m_editOps = new VMdEditOperations(this, m_file);
+    connect(m_editOps, &VEditOperations::statusMessage,
+            m_object, &VEditorObject::statusMessage);
+    connect(m_editOps, &VEditOperations::vimStatusUpdated,
+            m_object, &VEditorObject::vimStatusUpdated);
+
+    connect(this, &VPlainTextEdit::cursorPositionChanged,
+            this, &VMdEditor::updateCurrentHeader);
+
+    updateFontAndPalette();
+
+    updateConfig();
+}
+
+void VMdEditor::updateFontAndPalette()
+{
+    setFont(g_config->getMdEditFont());
+    setPalette(g_config->getMdEditPalette());
+}
+
+void VMdEditor::beginEdit()
+{
+    updateFontAndPalette();
+
+    updateConfig();
+
+    initInitImages();
+
+    setModified(false);
+
+    setReadOnlyAndHighlightCurrentLine(false);
+
+    emit statusChanged();
+
+    updateHeaders(m_mdHighlighter->getHeaderRegions());
+
+    if (m_freshEdit) {
+        m_freshEdit = false;
+        emit m_object->ready();
+    }
+}
+
+void VMdEditor::endEdit()
+{
+    setReadOnlyAndHighlightCurrentLine(true);
+    clearUnusedImages();
+}
+
+void VMdEditor::saveFile()
+{
+    Q_ASSERT(m_file->isModifiable());
+
+    if (!document()->isModified()) {
+        return;
+    }
+
+    m_file->setContent(toPlainText());
+    setModified(false);
+}
+
+void VMdEditor::reloadFile()
+{
+    const QString &content = m_file->getContent();
+    setPlainText(content);
+
+    setModified(false);
+}
+
+bool VMdEditor::scrollToBlock(int p_blockNumber)
+{
+    QTextBlock block = document()->findBlockByNumber(p_blockNumber);
+    if (block.isValid()) {
+        VEditUtils::scrollBlockInPage(this, block.blockNumber(), 0);
+        moveCursor(QTextCursor::EndOfBlock);
+        return true;
+    }
+
+    return false;
+}
+
+// Get the visual offset of a block.
+#define GETVISUALOFFSETY ((int)(contentOffset().y() + rect.y()))
+
+void VMdEditor::makeBlockVisible(const QTextBlock &p_block)
+{
+    if (!p_block.isValid() || !p_block.isVisible()) {
+        return;
+    }
+
+    QScrollBar *vbar = verticalScrollBar();
+    if (!vbar || !vbar->isVisible()) {
+        // No vertical scrollbar. No need to scroll.
+        return;
+    }
+
+    int height = rect().height();
+    QScrollBar *hbar = horizontalScrollBar();
+    if (hbar && hbar->isVisible()) {
+        height -= hbar->height();
+    }
+
+    bool moved = false;
+
+    QRectF rect = blockBoundingGeometry(p_block);
+    int y = GETVISUALOFFSETY;
+    int rectHeight = (int)rect.height();
+
+    // Handle the case rectHeight >= height.
+    if (rectHeight >= height) {
+        if (y <= 0) {
+            if (y + rectHeight < height) {
+                // Need to scroll up.
+                while (y + rectHeight < height && vbar->value() > vbar->minimum()) {
+                    moved = true;
+                    vbar->setValue(vbar->value() - vbar->singleStep());
+                    rect = blockBoundingGeometry(p_block);
+                    rectHeight = (int)rect.height();
+                    y = GETVISUALOFFSETY;
+                }
+            }
+        } else {
+            // Need to scroll down.
+            while (y > 0 && vbar->value() < vbar->maximum()) {
+                moved = true;
+                vbar->setValue(vbar->value() + vbar->singleStep());
+                rect = blockBoundingGeometry(p_block);
+                rectHeight = (int)rect.height();
+                y = GETVISUALOFFSETY;
+            }
+        }
+
+        if (moved) {
+            qDebug() << "scroll to make huge block visible";
+        }
+
+        return;
+    }
+
+    while (y < 0 && vbar->value() > vbar->minimum()) {
+        qDebug() << y << vbar->value() << vbar->minimum() << rectHeight;
+        moved = true;
+        vbar->setValue(vbar->value() - vbar->singleStep());
+        rect = blockBoundingGeometry(p_block);
+        rectHeight = (int)rect.height();
+        y = GETVISUALOFFSETY;
+    }
+
+    if (moved) {
+        qDebug() << "scroll page down to make block visible";
+        return;
+    }
+
+    while (y + rectHeight > height && vbar->value() < vbar->maximum()) {
+        moved = true;
+        vbar->setValue(vbar->value() + vbar->singleStep());
+        rect = blockBoundingGeometry(p_block);
+        rectHeight = (int)rect.height();
+        y = GETVISUALOFFSETY;
+    }
+
+    if (moved) {
+        qDebug() << "scroll page up to make block visible";
+    }
+}
+
+void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event)
+{
+    QMenu *menu = createStandardContextMenu();
+    menu->setToolTipsVisible(true);
+
+    const QList<QAction *> actions = menu->actions();
+
+    if (!textCursor().hasSelection()) {
+        VEditTab *editTab = dynamic_cast<VEditTab *>(parent());
+        Q_ASSERT(editTab);
+        if (editTab->isEditMode()) {
+            QAction *saveExitAct = new QAction(QIcon(":/resources/icons/save_exit.svg"),
+                                               tr("&Save Changes And Read"),
+                                               menu);
+            saveExitAct->setToolTip(tr("Save changes and exit edit mode"));
+            connect(saveExitAct, &QAction::triggered,
+                    this, [this]() {
+                        emit m_object->saveAndRead();
+                    });
+
+            QAction *discardExitAct = new QAction(QIcon(":/resources/icons/discard_exit.svg"),
+                                                  tr("&Discard Changes And Read"),
+                                                  menu);
+            discardExitAct->setToolTip(tr("Discard changes and exit edit mode"));
+            connect(discardExitAct, &QAction::triggered,
+                    this, [this]() {
+                        emit m_object->discardAndRead();
+                    });
+
+            menu->insertAction(actions.isEmpty() ? NULL : actions[0], discardExitAct);
+            menu->insertAction(discardExitAct, saveExitAct);
+            if (!actions.isEmpty()) {
+                menu->insertSeparator(actions[0]);
+            }
+        }
+    }
+
+    menu->exec(p_event->globalPos());
+    delete menu;
+}
+
+void VMdEditor::mousePressEvent(QMouseEvent *p_event)
+{
+    if (handleMousePressEvent(p_event)) {
+        return;
+    }
+
+    VPlainTextEdit::mousePressEvent(p_event);
+
+    emit m_object->selectionChangedByMouse(textCursor().hasSelection());
+}
+
+void VMdEditor::mouseReleaseEvent(QMouseEvent *p_event)
+{
+    if (handleMouseReleaseEvent(p_event)) {
+        return;
+    }
+
+    VPlainTextEdit::mousePressEvent(p_event);
+}
+
+void VMdEditor::mouseMoveEvent(QMouseEvent *p_event)
+{
+    if (handleMouseMoveEvent(p_event)) {
+        return;
+    }
+
+    VPlainTextEdit::mouseMoveEvent(p_event);
+
+    emit m_object->selectionChangedByMouse(textCursor().hasSelection());
+}
+
+QVariant VMdEditor::inputMethodQuery(Qt::InputMethodQuery p_query) const
+{
+    QVariant ret;
+    if (handleInputMethodQuery(p_query, ret)) {
+        return ret;
+    }
+
+    return VPlainTextEdit::inputMethodQuery(p_query);
+}
+
+bool VMdEditor::isBlockVisible(const QTextBlock &p_block)
+{
+    if (!p_block.isValid() || !p_block.isVisible()) {
+        return false;
+    }
+
+    QScrollBar *vbar = verticalScrollBar();
+    if (!vbar || !vbar->isVisible()) {
+        // No vertical scrollbar.
+        return true;
+    }
+
+    int height = rect().height();
+    QScrollBar *hbar = horizontalScrollBar();
+    if (hbar && hbar->isVisible()) {
+        height -= hbar->height();
+    }
+
+    QRectF rect = blockBoundingGeometry(p_block);
+    int y = GETVISUALOFFSETY;
+    int rectHeight = (int)rect.height();
+
+    return (y >= 0 && y < height) || (y < 0 && y + rectHeight > 0);
+}
+
+static void addHeaderSequence(QVector<int> &p_sequence, int p_level, int p_baseLevel)
+{
+    Q_ASSERT(p_level >= 1 && p_level < p_sequence.size());
+    if (p_level < p_baseLevel) {
+        p_sequence.fill(0);
+        return;
+    }
+
+    ++p_sequence[p_level];
+    for (int i = p_level + 1; i < p_sequence.size(); ++i) {
+        p_sequence[i] = 0;
+    }
+}
+
+static QString headerSequenceStr(const QVector<int> &p_sequence)
+{
+    QString res;
+    for (int i = 1; i < p_sequence.size(); ++i) {
+        if (p_sequence[i] != 0) {
+            res = res + QString::number(p_sequence[i]) + '.';
+        } else if (res.isEmpty()) {
+            continue;
+        } else {
+            break;
+        }
+    }
+
+    return res;
+}
+
+static void insertSequenceToHeader(QTextBlock p_block,
+                                   QRegExp &p_reg,
+                                   QRegExp &p_preReg,
+                                   const QString &p_seq)
+{
+    if (!p_block.isValid()) {
+        return;
+    }
+
+    QString text = p_block.text();
+    bool matched = p_reg.exactMatch(text);
+    Q_ASSERT(matched);
+
+    matched = p_preReg.exactMatch(text);
+    Q_ASSERT(matched);
+
+    int start = p_reg.cap(1).length() + 1;
+    int end = p_preReg.cap(1).length();
+
+    Q_ASSERT(start <= end);
+
+    QTextCursor cursor(p_block);
+    cursor.setPosition(p_block.position() + start);
+    if (start != end) {
+        cursor.setPosition(p_block.position() + end, QTextCursor::KeepAnchor);
+    }
+
+    if (p_seq.isEmpty()) {
+        cursor.removeSelectedText();
+    } else {
+        cursor.insertText(p_seq + ' ');
+    }
+}
+
+void VMdEditor::updateHeaders(const QVector<VElementRegion> &p_headerRegions)
+{
+    QTextDocument *doc = document();
+
+    QVector<VTableOfContentItem> headers;
+    QVector<int> headerBlockNumbers;
+    QVector<QString> headerSequences;
+    if (!p_headerRegions.isEmpty()) {
+        headers.reserve(p_headerRegions.size());
+        headerBlockNumbers.reserve(p_headerRegions.size());
+        headerSequences.reserve(p_headerRegions.size());
+    }
+
+    // Assume that each block contains only one line
+    // Only support # syntax for now
+    QRegExp headerReg(VUtils::c_headerRegExp);
+    int baseLevel = -1;
+    for (auto const & reg : p_headerRegions) {
+        QTextBlock block = doc->findBlock(reg.m_startPos);
+        if (!block.isValid()) {
+            continue;
+        }
+
+        if (!block.contains(reg.m_endPos - 1)) {
+            continue;
+        }
+
+        if ((block.userState() == HighlightBlockState::Normal)
+            && headerReg.exactMatch(block.text())) {
+            int level = headerReg.cap(1).length();
+            VTableOfContentItem header(headerReg.cap(2).trimmed(),
+                                       level,
+                                       block.blockNumber(),
+                                       headers.size());
+            headers.append(header);
+            headerBlockNumbers.append(block.blockNumber());
+            headerSequences.append(headerReg.cap(3));
+
+            if (baseLevel == -1) {
+                baseLevel = level;
+            } else if (baseLevel > level) {
+                baseLevel = level;
+            }
+        }
+    }
+
+    m_headers.clear();
+
+    bool autoSequence = m_config.m_enableHeadingSequence
+                        && !isReadOnly()
+                        && m_file->isModifiable();
+    int headingSequenceBaseLevel = g_config->getHeadingSequenceBaseLevel();
+    if (headingSequenceBaseLevel < 1 || headingSequenceBaseLevel > 6) {
+        headingSequenceBaseLevel = 1;
+    }
+
+    QVector<int> seqs(7, 0);
+    QRegExp preReg(VUtils::c_headerPrefixRegExp);
+    int curLevel = baseLevel - 1;
+    for (int i = 0; i < headers.size(); ++i) {
+        VTableOfContentItem &item = headers[i];
+        while (item.m_level > curLevel + 1) {
+            curLevel += 1;
+
+            // Insert empty level which is an invalid header.
+            m_headers.append(VTableOfContentItem(c_emptyHeaderName,
+                                                 curLevel,
+                                                 -1,
+                                                 m_headers.size()));
+            if (autoSequence) {
+                addHeaderSequence(seqs, curLevel, headingSequenceBaseLevel);
+            }
+        }
+
+        item.m_index = m_headers.size();
+        m_headers.append(item);
+        curLevel = item.m_level;
+        if (autoSequence) {
+            addHeaderSequence(seqs, item.m_level, headingSequenceBaseLevel);
+
+            QString seqStr = headerSequenceStr(seqs);
+            if (headerSequences[i] != seqStr) {
+                // Insert correct sequence.
+                insertSequenceToHeader(doc->findBlockByNumber(headerBlockNumbers[i]),
+                                       headerReg,
+                                       preReg,
+                                       seqStr);
+            }
+        }
+    }
+
+    emit headersChanged(m_headers);
+
+    updateCurrentHeader();
+}
+
+void VMdEditor::updateCurrentHeader()
+{
+    emit currentHeaderChanged(textCursor().block().blockNumber());
+}
+
+void VMdEditor::initInitImages()
+{
+    m_initImages = VUtils::fetchImagesFromMarkdownFile(m_file,
+                                                       ImageLink::LocalRelativeInternal);
+}
+
+void VMdEditor::clearUnusedImages()
+{
+    QVector<ImageLink> images = VUtils::fetchImagesFromMarkdownFile(m_file,
+                                                                    ImageLink::LocalRelativeInternal);
+
+    QVector<QString> unusedImages;
+
+    if (!m_insertedImages.isEmpty()) {
+        for (int i = 0; i < m_insertedImages.size(); ++i) {
+            const ImageLink &link = m_insertedImages[i];
+
+            if (link.m_type != ImageLink::LocalRelativeInternal) {
+                continue;
+            }
+
+            int j;
+            for (j = 0; j < images.size(); ++j) {
+                if (VUtils::equalPath(link.m_path, images[j].m_path)) {
+                    break;
+                }
+            }
+
+            // This inserted image is no longer in the file.
+            if (j == images.size()) {
+                unusedImages.push_back(link.m_path);
+            }
+        }
+
+        m_insertedImages.clear();
+    }
+
+    for (int i = 0; i < m_initImages.size(); ++i) {
+        const ImageLink &link = m_initImages[i];
+
+        V_ASSERT(link.m_type == ImageLink::LocalRelativeInternal);
+
+        int j;
+        for (j = 0; j < images.size(); ++j) {
+            if (VUtils::equalPath(link.m_path, images[j].m_path)) {
+                break;
+            }
+        }
+
+        // Original local relative image is no longer in the file.
+        if (j == images.size()) {
+            unusedImages.push_back(link.m_path);
+        }
+    }
+
+    if (!unusedImages.isEmpty()) {
+        if (g_config->getConfirmImagesCleanUp()) {
+            QVector<ConfirmItemInfo> items;
+            for (auto const & img : unusedImages) {
+                items.push_back(ConfirmItemInfo(img,
+                                                img,
+                                                img,
+                                                NULL));
+
+            }
+
+            QString text = tr("Following images seems not to be used in this note anymore. "
+                              "Please confirm the deletion of these images.");
+
+            QString info = tr("Deleted files could be found in the recycle "
+                              "bin of this note.<br>"
+                              "Click \"Cancel\" to leave them untouched.");
+
+            VConfirmDeletionDialog dialog(tr("Confirm Cleaning Up Unused Images"),
+                                          text,
+                                          info,
+                                          items,
+                                          true,
+                                          true,
+                                          true,
+                                          this);
+
+            unusedImages.clear();
+            if (dialog.exec()) {
+                items = dialog.getConfirmedItems();
+                g_config->setConfirmImagesCleanUp(dialog.getAskAgainEnabled());
+
+                for (auto const & item : items) {
+                    unusedImages.push_back(item.m_name);
+                }
+            }
+        }
+
+        for (int i = 0; i < unusedImages.size(); ++i) {
+            bool ret = false;
+            if (m_file->getType() == FileType::Note) {
+                const VNoteFile *tmpFile = dynamic_cast<const VNoteFile *>((VFile *)m_file);
+                ret = VUtils::deleteFile(tmpFile->getNotebook(), unusedImages[i], false);
+            } else if (m_file->getType() == FileType::Orphan) {
+                const VOrphanFile *tmpFile = dynamic_cast<const VOrphanFile *>((VFile *)m_file);
+                ret = VUtils::deleteFile(tmpFile, unusedImages[i], false);
+            } else {
+                Q_ASSERT(false);
+            }
+
+            if (!ret) {
+                qWarning() << "fail to delete unused original image" << unusedImages[i];
+            } else {
+                qDebug() << "delete unused image" << unusedImages[i];
+            }
+        }
+    }
+
+    m_initImages.clear();
+}
+
+void VMdEditor::keyPressEvent(QKeyEvent *p_event)
+{
+    if (m_editOps && m_editOps->handleKeyPressEvent(p_event)) {
+        return;
+    }
+
+    VPlainTextEdit::keyPressEvent(p_event);
+}
+
+bool VMdEditor::canInsertFromMimeData(const QMimeData *p_source) const
+{
+    return p_source->hasImage()
+           || p_source->hasUrls()
+           || VPlainTextEdit::canInsertFromMimeData(p_source);
+}
+
+void VMdEditor::insertFromMimeData(const QMimeData *p_source)
+{
+    VSelectDialog dialog(tr("Insert From Clipboard"), this);
+    dialog.addSelection(tr("Insert As Image"), 0);
+    dialog.addSelection(tr("Insert As Text"), 1);
+
+    if (p_source->hasImage()) {
+        // Image data in the clipboard
+        if (p_source->hasText()) {
+            if (dialog.exec() == QDialog::Accepted) {
+                if (dialog.getSelection() == 1) {
+                    // Insert as text.
+                    Q_ASSERT(p_source->hasText() && p_source->hasImage());
+                    VPlainTextEdit::insertFromMimeData(p_source);
+                    return;
+                }
+            } else {
+                return;
+            }
+        }
+
+        m_editOps->insertImageFromMimeData(p_source);
+        return;
+    } else if (p_source->hasUrls()) {
+        QList<QUrl> urls = p_source->urls();
+        if (urls.size() == 1 && VUtils::isImageURL(urls[0])) {
+            if (dialog.exec() == QDialog::Accepted) {
+                // FIXME: After calling dialog.exec(), p_source->hasUrl() returns false.
+                if (dialog.getSelection() == 0) {
+                    // Insert as image.
+                    m_editOps->insertImageFromURL(urls[0]);
+                    return;
+                }
+
+                QMimeData newSource;
+                newSource.setUrls(urls);
+                VPlainTextEdit::insertFromMimeData(&newSource);
+                return;
+            } else {
+                return;
+            }
+        }
+    } else if (p_source->hasText()) {
+        QString text = p_source->text();
+        if (VUtils::isImageURLText(text)) {
+            // The text is a URL to an image.
+            if (dialog.exec() == QDialog::Accepted) {
+                if (dialog.getSelection() == 0) {
+                    // Insert as image.
+                    QUrl url(text);
+                    if (url.isValid()) {
+                        m_editOps->insertImageFromURL(QUrl(text));
+                    }
+                    return;
+                }
+            } else {
+                return;
+            }
+        }
+
+        Q_ASSERT(p_source->hasText());
+    }
+
+    VPlainTextEdit::insertFromMimeData(p_source);
+}
+
+void VMdEditor::imageInserted(const QString &p_path)
+{
+    ImageLink link;
+    link.m_path = p_path;
+    if (m_file->useRelativeImageFolder()) {
+        link.m_type = ImageLink::LocalRelativeInternal;
+    } else {
+        link.m_type = ImageLink::LocalAbsolute;
+    }
+
+    m_insertedImages.append(link);
+}
+
+bool VMdEditor::scrollToHeader(int p_blockNumber)
+{
+    if (p_blockNumber < 0) {
+        return false;
+    }
+
+    return scrollToBlock(p_blockNumber);
+}
+
+int VMdEditor::indexOfCurrentHeader() const
+{
+    if (m_headers.isEmpty()) {
+        return -1;
+    }
+
+    int blockNumber = textCursor().block().blockNumber();
+    for (int i = m_headers.size() - 1; i >= 0; --i) {
+        if (!m_headers[i].isEmpty()
+            && m_headers[i].m_blockNumber <= blockNumber) {
+            return i;
+        }
+    }
+
+    return -1;
+}
+
+bool VMdEditor::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat)
+{
+    if (m_headers.isEmpty()) {
+        return false;
+    }
+
+    QTextCursor cursor = textCursor();
+    int cursorLine = cursor.block().blockNumber();
+    int targetIdx = -1;
+    // -1: skip level check.
+    int targetLevel = 0;
+    int idx = indexOfCurrentHeader();
+    if (idx == -1) {
+        // Cursor locates at the beginning, before any headers.
+        if (p_relativeLevel < 0 || !p_forward) {
+            return false;
+        }
+    }
+
+    int delta = 1;
+    if (!p_forward) {
+        delta = -1;
+    }
+
+    bool firstHeader = true;
+    for (targetIdx = idx == -1 ? 0 : idx;
+         targetIdx >= 0 && targetIdx < m_headers.size();
+         targetIdx += delta) {
+        const VTableOfContentItem &header = m_headers[targetIdx];
+        if (header.isEmpty()) {
+            continue;
+        }
+
+        if (targetLevel == 0) {
+            // The target level has not been init yet.
+            Q_ASSERT(firstHeader);
+            targetLevel = header.m_level;
+            if (p_relativeLevel < 0) {
+                targetLevel += p_relativeLevel;
+                if (targetLevel < 1) {
+                    // Invalid level.
+                    return false;
+                }
+            } else if (p_relativeLevel > 0) {
+                targetLevel = -1;
+            }
+        }
+
+        if (targetLevel == -1 || header.m_level == targetLevel) {
+            if (firstHeader
+                && (cursorLine == header.m_blockNumber
+                    || p_forward)
+                && idx != -1) {
+                // This header is not counted for the repeat.
+                firstHeader = false;
+                continue;
+            }
+
+            if (--p_repeat == 0) {
+                // Found.
+                break;
+            }
+        } else if (header.m_level < targetLevel) {
+            // Stop by higher level.
+            return false;
+        }
+
+        firstHeader = false;
+    }
+
+    if (targetIdx < 0 || targetIdx >= m_headers.size()) {
+        return false;
+    }
+
+    // Jump to target header.
+    int line = m_headers[targetIdx].m_blockNumber;
+    if (line > -1) {
+        QTextBlock block = document()->findBlockByNumber(line);
+        if (block.isValid()) {
+            cursor.setPosition(block.position());
+            setTextCursor(cursor);
+            return true;
+        }
+    }
+
+    return false;
+}
+
+void VMdEditor::scrollBlockInPage(int p_blockNum, int p_dest)
+{
+    VEditUtils::scrollBlockInPage(this, p_blockNum, p_dest);
+}
+
+void VMdEditor::updatePlainTextEditConfig()
+{
+    m_previewMgr->setPreviewEnabled(g_config->getEnablePreviewImages());
+    setBlockImageEnabled(g_config->getEnablePreviewImages());
+
+    setImageWidthConstrainted(g_config->getEnablePreviewImageConstraint());
+
+    int lineNumber = g_config->getEditorLineNumber();
+    if (lineNumber < (int)LineNumberType::None || lineNumber >= (int)LineNumberType::Invalid) {
+        lineNumber = (int)LineNumberType::None;
+    }
+
+    setLineNumberType((LineNumberType)lineNumber);
+    setLineNumberColor(g_config->getEditorLineNumberFg(),
+                       g_config->getEditorLineNumberBg());
+}
+
+void VMdEditor::updateConfig()
+{
+    updatePlainTextEditConfig();
+    updateEditConfig();
+}

+ 212 - 0
src/vmdeditor.h

@@ -0,0 +1,212 @@
+#ifndef VMDEDITOR_H
+#define VMDEDITOR_H
+
+#include <QVector>
+#include <QString>
+#include <QClipboard>
+#include <QImage>
+
+#include "vplaintextedit.h"
+#include "veditor.h"
+#include "vconfigmanager.h"
+#include "vtableofcontent.h"
+#include "veditoperations.h"
+#include "vconfigmanager.h"
+#include "utils/vutils.h"
+
+class HGMarkdownHighlighter;
+class VCodeBlockHighlightHelper;
+class VDocument;
+class VPreviewManager;
+
+class VMdEditor : public VPlainTextEdit, public VEditor
+{
+    Q_OBJECT
+public:
+    VMdEditor(VFile *p_file,
+              VDocument *p_doc,
+              MarkdownConverterType p_type,
+              QWidget *p_parent = nullptr);
+
+    void beginEdit() Q_DECL_OVERRIDE;
+
+    void endEdit() Q_DECL_OVERRIDE;
+
+    void saveFile() Q_DECL_OVERRIDE;
+
+    void reloadFile() Q_DECL_OVERRIDE;
+
+    bool scrollToBlock(int p_blockNumber) Q_DECL_OVERRIDE;
+
+    void makeBlockVisible(const QTextBlock &p_block) Q_DECL_OVERRIDE;
+
+    QVariant inputMethodQuery(Qt::InputMethodQuery p_query) const Q_DECL_OVERRIDE;
+
+    bool isBlockVisible(const QTextBlock &p_block) Q_DECL_OVERRIDE;
+
+    // An image has been inserted. The image is relative.
+    // @p_path is the absolute path of the inserted image.
+    void imageInserted(const QString &p_path);
+
+    // Scroll to header @p_blockNumber.
+    // Return true if @p_blockNumber is valid to scroll to.
+    bool scrollToHeader(int p_blockNumber);
+
+    void scrollBlockInPage(int p_blockNum, int p_dest) Q_DECL_OVERRIDE;
+
+    void updateConfig() Q_DECL_OVERRIDE;
+
+public slots:
+    bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE;
+
+// Wrapper functions for QPlainTextEdit/QTextEdit.
+public:
+    void setExtraSelectionsW(const QList<QTextEdit::ExtraSelection> &p_selections) Q_DECL_OVERRIDE
+    {
+        setExtraSelections(p_selections);
+    }
+
+    QTextDocument *documentW() const Q_DECL_OVERRIDE
+    {
+        return document();
+    }
+
+    void setTabStopWidthW(int p_width) Q_DECL_OVERRIDE
+    {
+        setTabStopWidth(p_width);
+    }
+
+    QTextCursor textCursorW() const Q_DECL_OVERRIDE
+    {
+        return textCursor();
+    }
+
+    void moveCursorW(QTextCursor::MoveOperation p_operation,
+                     QTextCursor::MoveMode p_mode) Q_DECL_OVERRIDE
+    {
+        moveCursor(p_operation, p_mode);
+    }
+
+    QScrollBar *verticalScrollBarW() const Q_DECL_OVERRIDE
+    {
+        return verticalScrollBar();
+    }
+
+    QScrollBar *horizontalScrollBarW() const Q_DECL_OVERRIDE
+    {
+        return horizontalScrollBar();
+    }
+
+    void setTextCursorW(const QTextCursor &p_cursor) Q_DECL_OVERRIDE
+    {
+        setTextCursor(p_cursor);
+    }
+
+    bool findW(const QString &p_exp,
+               QTextDocument::FindFlags p_options = QTextDocument::FindFlags()) Q_DECL_OVERRIDE
+    {
+        return find(p_exp, p_options);
+    }
+
+    bool findW(const QRegExp &p_exp,
+               QTextDocument::FindFlags p_options = QTextDocument::FindFlags()) Q_DECL_OVERRIDE
+    {
+        return find(p_exp, p_options);
+    }
+
+    void setReadOnlyW(bool p_ro) Q_DECL_OVERRIDE
+    {
+        setReadOnly(p_ro);
+    }
+
+    QWidget *viewportW() const Q_DECL_OVERRIDE
+    {
+        return viewport();
+    }
+
+    void insertPlainTextW(const QString &p_text) Q_DECL_OVERRIDE
+    {
+        insertPlainText(p_text);
+    }
+
+    void undoW() Q_DECL_OVERRIDE
+    {
+        undo();
+    }
+
+    void redoW() Q_DECL_OVERRIDE
+    {
+        redo();
+    }
+
+signals:
+    // Signal when headers change.
+    void headersChanged(const QVector<VTableOfContentItem> &p_headers);
+
+    // Signal when current header change.
+    void currentHeaderChanged(int p_blockNumber);
+
+    // Signal when the status of VMdEdit changed.
+    // Will be emitted by VImagePreviewer for now.
+    void statusChanged();
+
+protected:
+    void updateFontAndPalette() Q_DECL_OVERRIDE;
+
+    void contextMenuEvent(QContextMenuEvent *p_event) Q_DECL_OVERRIDE;
+
+    // Used to implement dragging mouse with Ctrl and left button pressed to scroll.
+    void mousePressEvent(QMouseEvent *p_event) Q_DECL_OVERRIDE;
+
+    void mouseReleaseEvent(QMouseEvent *p_event) Q_DECL_OVERRIDE;
+
+    void mouseMoveEvent(QMouseEvent *p_event) Q_DECL_OVERRIDE;
+
+    void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE;
+
+    bool canInsertFromMimeData(const QMimeData *p_source) const Q_DECL_OVERRIDE;
+
+    void insertFromMimeData(const QMimeData *p_source) Q_DECL_OVERRIDE;
+
+private slots:
+    // Update m_headers according to elements.
+    void updateHeaders(const QVector<VElementRegion> &p_headerRegions);
+
+    // Update current header according to cursor position.
+    // When there is no header in current cursor, will signal an invalid header.
+    void updateCurrentHeader();
+
+private:
+    // Update the config of VPlainTextEdit according to global configurations.
+    void updatePlainTextEditConfig();
+
+    // Get the initial images from file before edit.
+    void initInitImages();
+
+    // Clear two kind of images according to initial images and current images:
+    // 1. Newly inserted images which are deleted later;
+    // 2. Initial images which are deleted;
+    void clearUnusedImages();
+
+    // Index in m_headers of current header which contains the cursor.
+    int indexOfCurrentHeader() const;
+
+    HGMarkdownHighlighter *m_mdHighlighter;
+
+    VCodeBlockHighlightHelper *m_cbHighlighter;
+
+    VPreviewManager *m_previewMgr;
+
+    // Image links inserted while editing.
+    QVector<ImageLink> m_insertedImages;
+
+    // Image links right at the beginning of the edit.
+    QVector<ImageLink> m_initImages;
+
+    // Mainly used for title jump.
+    QVector<VTableOfContentItem> m_headers;
+
+    bool m_freshEdit;
+};
+
+#endif // VMDEDITOR_H

+ 21 - 18
src/vmdtab.cpp

@@ -12,11 +12,14 @@
 #include "vmarkdownconverter.h"
 #include "vnotebook.h"
 #include "vtableofcontent.h"
-#include "vmdedit.h"
 #include "dialog/vfindreplacedialog.h"
 #include "veditarea.h"
 #include "vconstants.h"
 #include "vwebview.h"
+#include "vmdeditor.h"
+#include "vmainwindow.h"
+
+extern VMainWindow *g_mainWin;
 
 extern VConfigManager *g_config;
 
@@ -124,7 +127,7 @@ bool VMdTab::scrollEditorToHeader(const VHeaderPointer &p_header)
         return false;
     }
 
-    VMdEdit *mdEdit = dynamic_cast<VMdEdit *>(getEditor());
+    VMdEditor *mdEdit = getEditor();
 
     int blockNumber = -1;
     if (p_header.isValid()) {
@@ -185,8 +188,7 @@ void VMdTab::showFileEditMode()
 
     m_isEditMode = true;
 
-    VMdEdit *mdEdit = dynamic_cast<VMdEdit *>(getEditor());
-    V_ASSERT(mdEdit);
+    VMdEditor *mdEdit = getEditor();
 
     mdEdit->beginEdit();
     m_stacks->setCurrentWidget(mdEdit);
@@ -376,34 +378,35 @@ void VMdTab::setupMarkdownViewer()
 void VMdTab::setupMarkdownEditor()
 {
     Q_ASSERT(!m_editor);
-    qDebug() << "create Markdown editor";
 
-    m_editor = new VMdEdit(m_file, m_document, m_mdConType, this);
-    connect(dynamic_cast<VMdEdit *>(m_editor), &VMdEdit::headersChanged,
+    m_editor = new VMdEditor(m_file, m_document, m_mdConType, this);
+    connect(m_editor, &VMdEditor::headersChanged,
             this, &VMdTab::updateOutlineFromHeaders);
-    connect(dynamic_cast<VMdEdit *>(m_editor), SIGNAL(currentHeaderChanged(int)),
+    connect(m_editor, SIGNAL(currentHeaderChanged(int)),
             this, SLOT(updateCurrentHeader(int)));
-    connect(dynamic_cast<VMdEdit *>(m_editor), &VMdEdit::statusChanged,
+    connect(m_editor, &VMdEditor::statusChanged,
             this, &VMdTab::updateStatus);
-    connect(m_editor, &VEdit::textChanged,
+    connect(m_editor, &VMdEditor::textChanged,
             this, &VMdTab::updateStatus);
-    connect(m_editor, &VEdit::cursorPositionChanged,
+    connect(m_editor, &VMdEditor::cursorPositionChanged,
             this, &VMdTab::updateStatus);
-    connect(m_editor, &VEdit::saveAndRead,
+    connect(g_mainWin, &VMainWindow::editorConfigUpdated,
+            m_editor, &VMdEditor::updateConfig);
+    connect(m_editor->object(), &VEditorObject::saveAndRead,
             this, &VMdTab::saveAndRead);
-    connect(m_editor, &VEdit::discardAndRead,
+    connect(m_editor->object(), &VEditorObject::discardAndRead,
             this, &VMdTab::discardAndRead);
-    connect(m_editor, &VEdit::saveNote,
+    connect(m_editor->object(), &VEditorObject::saveNote,
             this, &VMdTab::saveFile);
-    connect(m_editor, &VEdit::statusMessage,
+    connect(m_editor->object(), &VEditorObject::statusMessage,
             this, &VEditTab::statusMessage);
-    connect(m_editor, &VEdit::vimStatusUpdated,
+    connect(m_editor->object(), &VEditorObject::vimStatusUpdated,
             this, &VEditTab::vimStatusUpdated);
-    connect(m_editor, &VEdit::requestCloseFindReplaceDialog,
+    connect(m_editor->object(), &VEditorObject::requestCloseFindReplaceDialog,
             this, [this]() {
                 this->m_editArea->getFindReplaceDialog()->closeDialog();
             });
-    connect(m_editor, SIGNAL(ready(void)),
+    connect(m_editor->object(), SIGNAL(ready(void)),
             this, SLOT(restoreFromTabInfo(void)));
 
     enableHeadingSequence(m_enableHeadingSequence);

+ 6 - 6
src/vmdtab.h

@@ -10,8 +10,8 @@
 
 class VWebView;
 class QStackedLayout;
-class VEdit;
 class VDocument;
+class VMdEditor;
 
 class VMdTab : public VEditTab
 {
@@ -55,7 +55,7 @@ public:
 
     VWebView *getWebViewer() const;
 
-    VEdit *getEditor() const;
+    VMdEditor *getEditor() const;
 
     MarkdownConverterType getMarkdownConverterType() const;
 
@@ -148,13 +148,13 @@ private:
     void focusChild() Q_DECL_OVERRIDE;
 
     // Get the markdown editor. If not init yet, init and return it.
-    VEdit *getEditor();
+    VMdEditor *getEditor();
 
     // Restore from @p_fino.
     // Return true if succeed.
     bool restoreFromTabInfo(const VEditTabInfo &p_info) Q_DECL_OVERRIDE;
 
-    VEdit *m_editor;
+    VMdEditor *m_editor;
     VWebView *m_webViewer;
     VDocument *m_document;
     MarkdownConverterType m_mdConType;
@@ -165,7 +165,7 @@ private:
     QStackedLayout *m_stacks;
 };
 
-inline VEdit *VMdTab::getEditor()
+inline VMdEditor *VMdTab::getEditor()
 {
     if (m_editor) {
         return m_editor;
@@ -175,7 +175,7 @@ inline VEdit *VMdTab::getEditor()
     }
 }
 
-inline VEdit *VMdTab::getEditor() const
+inline VMdEditor *VMdTab::getEditor() const
 {
     return m_editor;
 }

+ 30 - 9
src/vplaintextedit.cpp

@@ -14,10 +14,11 @@ const int VPlainTextEdit::c_minimumImageWidth = 100;
 
 enum class BlockState
 {
-    Normal = 1,
+    Normal = 0,
     CodeBlockStart,
     CodeBlock,
-    CodeBlockEnd
+    CodeBlockEnd,
+    Comment
 };
 
 
@@ -79,6 +80,8 @@ void VPlainTextEdit::updateBlockImages(const QVector<VBlockImageInfo> &p_blocksI
 {
     if (m_blockImageEnabled) {
         m_imageMgr->updateBlockInfos(p_blocksInfo, m_maximumImageWidth);
+
+        update();
     }
 }
 
@@ -298,6 +301,9 @@ void VPlainTextEdit::drawImageOfBlock(const QTextBlock &p_block,
                      qMax(info->m_imageHeight, tmpRect.height() - oriHeight));
 
     p_painter->drawPixmap(targetRect, *image);
+
+    auto *layout = getLayout();
+    emit layout->documentSizeChanged(layout->documentSize());
 }
 
 QRectF VPlainTextEdit::originalBlockBoundingRect(const QTextBlock &p_block) const
@@ -322,14 +328,23 @@ void VPlainTextEdit::setBlockImageEnabled(bool p_enabled)
 void VPlainTextEdit::setImageWidthConstrainted(bool p_enabled)
 {
     m_imageWidthConstrainted = p_enabled;
+
+    updateImageWidth();
+
+    auto *layout = getLayout();
+    emit layout->documentSizeChanged(layout->documentSize());
 }
 
-void VPlainTextEdit::resizeEvent(QResizeEvent *p_event)
+void VPlainTextEdit::updateImageWidth()
 {
     bool needUpdate = false;
     if (m_imageWidthConstrainted) {
-        const QSize &si = p_event->size();
-        m_maximumImageWidth = si.width();
+        int viewWidth = viewport()->size().width();
+        m_maximumImageWidth = viewWidth - 10;
+        if (m_maximumImageWidth < 0) {
+            m_maximumImageWidth = viewWidth;
+        }
+
         needUpdate = true;
     } else if (m_maximumImageWidth != INT_MAX) {
         needUpdate = true;
@@ -339,6 +354,11 @@ void VPlainTextEdit::resizeEvent(QResizeEvent *p_event)
     if (needUpdate) {
         m_imageMgr->updateImageWidth(m_maximumImageWidth);
     }
+}
+
+void VPlainTextEdit::resizeEvent(QResizeEvent *p_event)
+{
+    updateImageWidth();
 
     QPlainTextEdit::resizeEvent(p_event);
 
@@ -368,9 +388,8 @@ void VPlainTextEdit::paintLineNumberArea(QPaintEvent *p_event)
     }
 
     int blockNumber = block.blockNumber();
-    int offsetY = (int)contentOffset().y();
-    QRectF rect = blockBoundingRect(block);
-    int top = offsetY + (int)rect.y();
+    QRectF rect = blockBoundingGeometry(block);
+    int top = (int)(contentOffset().y() + rect.y());
     int bottom = top + (int)rect.height();
     int eventTop = p_event->rect().top();
     int eventBtm = p_event->rect().bottom();
@@ -496,7 +515,9 @@ void VPlainTextEdit::updateLineNumberAreaMargin()
         width = m_lineNumberArea->calculateWidth();
     }
 
-    setViewportMargins(width, 0, 0, 0);
+    if (width != viewportMargins().left()) {
+        setViewportMargins(width, 0, 0, 0);
+    }
 }
 
 void VPlainTextEdit::updateLineNumberArea()

+ 11 - 0
src/vplaintextedit.h

@@ -83,6 +83,8 @@ public:
 
     void setLineNumberType(LineNumberType p_type);
 
+    void setLineNumberColor(const QColor &p_foreground, const QColor &p_background);
+
     // The minimum width of an image in pixels.
     static const int c_minimumImageWidth;
 
@@ -111,6 +113,8 @@ private:
 
     VPlainTextDocumentLayout *getLayout() const;
 
+    void updateImageWidth();
+
     // Widget to display line number area.
     VLineNumberArea *m_lineNumberArea;
 
@@ -176,4 +180,11 @@ inline void VPlainTextEdit::setLineNumberType(LineNumberType p_type)
     updateLineNumberArea();
 }
 
+inline void VPlainTextEdit::setLineNumberColor(const QColor &p_foreground,
+                                               const QColor &p_background)
+{
+    m_lineNumberArea->setForegroundColor(p_foreground);
+    m_lineNumberArea->setBackgroundColor(p_background);
+}
+
 #endif // VPLAINTEXTEDIT_H

+ 316 - 0
src/vpreviewmanager.cpp

@@ -0,0 +1,316 @@
+#include "vpreviewmanager.h"
+
+#include <QTextDocument>
+#include <QDebug>
+#include <QDir>
+#include <QUrl>
+#include <QVector>
+#include "vconfigmanager.h"
+#include "utils/vutils.h"
+#include "vdownloader.h"
+#include "hgmarkdownhighlighter.h"
+#include "vtextblockdata.h"
+
+extern VConfigManager *g_config;
+
+VPreviewManager::VPreviewManager(VMdEditor *p_editor)
+    : QObject(p_editor),
+      m_editor(p_editor),
+      m_previewEnabled(false)
+{
+    m_blockImageInfo.resize(PreviewSource::Invalid);
+
+    m_downloader = new VDownloader(this);
+    connect(m_downloader, &VDownloader::downloadFinished,
+            this, &VPreviewManager::imageDownloaded);
+}
+
+void VPreviewManager::imageLinksUpdated(const QVector<VElementRegion> &p_imageRegions)
+{
+    if (!m_previewEnabled) {
+        return;
+    }
+
+    m_imageRegions = p_imageRegions;
+
+    previewImages();
+}
+
+void VPreviewManager::imageDownloaded(const QByteArray &p_data, const QString &p_url)
+{
+    if (!m_previewEnabled) {
+        return;
+    }
+
+    auto it = m_urlToName.find(p_url);
+    if (it == m_urlToName.end()) {
+        return;
+    }
+
+    QString name = it.value();
+    m_urlToName.erase(it);
+
+    if (m_editor->containsImage(name) || name.isEmpty()) {
+        return;
+    }
+
+    QPixmap image;
+    image.loadFromData(p_data);
+
+    if (!image.isNull()) {
+        m_editor->addImage(name, image);
+        qDebug() << "downloaded image inserted in resource manager" << p_url << name;
+        emit requestUpdateImageLinks();
+    }
+}
+
+void VPreviewManager::setPreviewEnabled(bool p_enabled)
+{
+    if (m_previewEnabled != p_enabled) {
+        m_previewEnabled = p_enabled;
+
+        if (!m_previewEnabled) {
+            clearPreview();
+        }
+    }
+}
+
+void VPreviewManager::clearPreview()
+{
+    for (int i = 0; i < m_blockImageInfo.size(); ++i) {
+        m_blockImageInfo[i].clear();
+    }
+
+    updateEditorBlockImages();
+}
+
+void VPreviewManager::previewImages()
+{
+    QVector<ImageLinkInfo> imageLinks;
+    fetchImageLinksFromRegions(imageLinks);
+
+    updateBlockImageInfo(imageLinks);
+
+    updateEditorBlockImages();
+}
+
+// Returns true if p_text[p_start, p_end) is all spaces.
+static bool isAllSpaces(const QString &p_text, int p_start, int p_end)
+{
+    int len = qMin(p_text.size(), p_end);
+    for (int i = p_start; i < len; ++i) {
+        if (!p_text[i].isSpace()) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+void VPreviewManager::fetchImageLinksFromRegions(QVector<ImageLinkInfo> &p_imageLinks)
+{
+    p_imageLinks.clear();
+
+    if (m_imageRegions.isEmpty()) {
+        return;
+    }
+
+    p_imageLinks.reserve(m_imageRegions.size());
+
+    QTextDocument *doc = m_editor->document();
+
+    for (int i = 0; i < m_imageRegions.size(); ++i) {
+        VElementRegion &reg = m_imageRegions[i];
+        QTextBlock block = doc->findBlock(reg.m_startPos);
+        if (!block.isValid()) {
+            continue;
+        }
+
+        int blockStart = block.position();
+        int blockEnd = blockStart + block.length() - 1;
+        QString text = block.text();
+        Q_ASSERT(reg.m_endPos <= blockEnd);
+        ImageLinkInfo info(reg.m_startPos,
+                           reg.m_endPos,
+                           block.blockNumber(),
+                           calculateBlockMargin(block));
+        if ((reg.m_startPos == blockStart
+             || isAllSpaces(text, 0, reg.m_startPos - blockStart))
+            && (reg.m_endPos == blockEnd
+                || isAllSpaces(text, reg.m_endPos - blockStart, blockEnd - blockStart))) {
+            // Image block.
+            info.m_isBlock = true;
+            info.m_linkUrl = fetchImagePathToPreview(text, info.m_linkShortUrl);
+        } else {
+            // Inline image.
+            info.m_isBlock = false;
+            info.m_linkUrl = fetchImagePathToPreview(text.mid(reg.m_startPos - blockStart,
+                                                              reg.m_endPos - reg.m_startPos),
+                                                     info.m_linkShortUrl);
+        }
+
+        if (info.m_linkUrl.isEmpty()) {
+            continue;
+        }
+
+        p_imageLinks.append(info);
+
+        qDebug() << "image region" << i
+                 << info.m_startPos << info.m_endPos << info.m_blockNumber
+                 << info.m_linkShortUrl << info.m_linkUrl << info.m_isBlock;
+    }
+}
+
+QString VPreviewManager::fetchImageUrlToPreview(const QString &p_text)
+{
+    QRegExp regExp(VUtils::c_imageLinkRegExp);
+
+    int index = regExp.indexIn(p_text);
+    if (index == -1) {
+        return QString();
+    }
+
+    int lastIndex = regExp.lastIndexIn(p_text);
+    if (lastIndex != index) {
+        return QString();
+    }
+
+    return regExp.capturedTexts()[2].trimmed();
+}
+
+QString VPreviewManager::fetchImagePathToPreview(const QString &p_text, QString &p_url)
+{
+    p_url = fetchImageUrlToPreview(p_text);
+    if (p_url.isEmpty()) {
+        return p_url;
+    }
+
+    const VFile *file = m_editor->getFile();
+
+    QString imagePath;
+    QFileInfo info(file->fetchBasePath(), p_url);
+
+    if (info.exists()) {
+        if (info.isNativePath()) {
+            // Local file.
+            imagePath = QDir::cleanPath(info.absoluteFilePath());
+        } else {
+            imagePath = p_url;
+        }
+    } else {
+        QString decodedUrl(p_url);
+        VUtils::decodeUrl(decodedUrl);
+        QFileInfo dinfo(file->fetchBasePath(), decodedUrl);
+        if (dinfo.exists()) {
+            if (dinfo.isNativePath()) {
+                // Local file.
+                imagePath = QDir::cleanPath(dinfo.absoluteFilePath());
+            } else {
+                imagePath = p_url;
+            }
+        } else {
+            QUrl url(p_url);
+            imagePath = url.toString();
+        }
+    }
+
+    return imagePath;
+}
+
+void VPreviewManager::updateBlockImageInfo(const QVector<ImageLinkInfo> &p_imageLinks)
+{
+    QVector<VBlockImageInfo> &blockInfos = m_blockImageInfo[PreviewSource::ImageLink];
+    blockInfos.clear();
+
+    for (int i = 0; i < p_imageLinks.size(); ++i) {
+        const ImageLinkInfo &link = p_imageLinks[i];
+
+        // Skip inline images.
+        if (!link.m_isBlock) {
+            continue;
+        }
+
+        QString name = imageResourceName(link);
+        if (name.isEmpty()) {
+            continue;
+        }
+
+        VBlockImageInfo info(link.m_blockNumber, name, link.m_margin);
+        blockInfos.push_back(info);
+    }
+}
+
+QString VPreviewManager::imageResourceName(const ImageLinkInfo &p_link)
+{
+    QString name = p_link.m_linkShortUrl;
+    if (m_editor->containsImage(name)
+        || name.isEmpty()) {
+        return name;
+    }
+
+    // Add it to the resource.
+    QString imgPath = p_link.m_linkUrl;
+    QFileInfo info(imgPath);
+    QPixmap image;
+    if (info.exists()) {
+        // Local file.
+        image = QPixmap(imgPath);
+    } else {
+        // URL. Try to download it.
+        m_downloader->download(imgPath);
+        m_urlToName.insert(imgPath, name);
+    }
+
+    if (image.isNull()) {
+        return QString();
+    }
+
+    m_editor->addImage(name, image);
+    return name;
+}
+
+void VPreviewManager::updateEditorBlockImages()
+{
+    // TODO: need to combine all preview sources.
+    Q_ASSERT(m_blockImageInfo.size() == 1);
+
+    m_editor->updateBlockImages(m_blockImageInfo[PreviewSource::ImageLink]);
+}
+
+int VPreviewManager::calculateBlockMargin(const QTextBlock &p_block)
+{
+    static QHash<QString, int> spaceWidthOfFonts;
+
+    if (!p_block.isValid()) {
+        return 0;
+    }
+
+    QString text = p_block.text();
+    int nrSpaces = 0;
+    for (int i = 0; i < text.size(); ++i) {
+        if (!text[i].isSpace()) {
+            break;
+        } else if (text[i] == ' ') {
+            ++nrSpaces;
+        } else if (text[i] == '\t') {
+            nrSpaces += m_editor->tabStopWidth();
+        }
+    }
+
+    if (nrSpaces == 0) {
+        return 0;
+    }
+
+    int spaceWidth = 0;
+    QFont font = p_block.charFormat().font();
+    QString fontName = font.toString();
+    auto it = spaceWidthOfFonts.find(fontName);
+    if (it != spaceWidthOfFonts.end()) {
+        spaceWidth = it.value();
+    } else {
+        spaceWidth = QFontMetrics(font).width(' ');
+        spaceWidthOfFonts.insert(fontName, spaceWidth);
+    }
+
+    return spaceWidth * nrSpaces;
+}

+ 135 - 0
src/vpreviewmanager.h

@@ -0,0 +1,135 @@
+#ifndef VPREVIEWMANAGER_H
+#define VPREVIEWMANAGER_H
+
+#include <QObject>
+#include <QString>
+#include <QTextBlock>
+#include <QHash>
+#include <QVector>
+#include "hgmarkdownhighlighter.h"
+#include "vmdeditor.h"
+
+class VDownloader;
+
+
+class VPreviewManager : public QObject
+{
+    Q_OBJECT
+public:
+    explicit VPreviewManager(VMdEditor *p_editor);
+
+    void setPreviewEnabled(bool p_enabled);
+
+    // Clear all the preview.
+    void clearPreview();
+
+public slots:
+    // Image links were updated from the highlighter.
+    void imageLinksUpdated(const QVector<VElementRegion> &p_imageRegions);
+
+signals:
+    // Request highlighter to update image links.
+    void requestUpdateImageLinks();
+
+private slots:
+    // Non-local image downloaded for preview.
+    void imageDownloaded(const QByteArray &p_data, const QString &p_url);
+
+private:
+    // Sources of the preview.
+    enum PreviewSource
+    {
+        ImageLink = 0,
+        Invalid
+    };
+
+    struct ImageLinkInfo
+    {
+        ImageLinkInfo()
+            : m_startPos(-1),
+              m_endPos(-1),
+              m_blockNumber(-1),
+              m_margin(0),
+              m_isBlock(false)
+        {
+        }
+
+        ImageLinkInfo(int p_startPos,
+                      int p_endPos,
+                      int p_blockNumber,
+                      int p_margin)
+            : m_startPos(p_startPos),
+              m_endPos(p_endPos),
+              m_blockNumber(p_blockNumber),
+              m_margin(p_margin),
+              m_isBlock(false)
+        {
+        }
+
+        int m_startPos;
+
+        int m_endPos;
+
+        int m_blockNumber;
+
+        // Left margin of this block in pixels.
+        int m_margin;
+
+        // Short URL within the () of ![]().
+        // Used as the ID of the image.
+        QString m_linkShortUrl;
+
+        // Full URL of the link.
+        QString m_linkUrl;
+
+        // Whether it is an image block.
+        bool m_isBlock;
+    };
+
+    // Start to preview images according to image links.
+    void previewImages();
+
+    // According to m_imageRegions, fetch the image link Url.
+    // @p_imageRegions: output.
+    void fetchImageLinksFromRegions(QVector<ImageLinkInfo> &p_imageLinks);
+
+    // Fetch the image link's URL if there is only one link.
+    QString fetchImageUrlToPreview(const QString &p_text);
+
+    // Fetch teh image's full path if there is only one image link.
+    // @p_url: contains the short URL in ![]().
+    QString fetchImagePathToPreview(const QString &p_text, QString &p_url);
+
+    void updateBlockImageInfo(const QVector<ImageLinkInfo> &p_imageLinks);
+
+    // Get the name of the image in the resource manager.
+    // Will add the image to the resource manager if not exists.
+    // Returns empty if fail to add the image to the resource manager.
+    QString imageResourceName(const ImageLinkInfo &p_link);
+
+    // Ask the editor to preview images.
+    void updateEditorBlockImages();
+
+    // Calculate the block margin (prefix spaces) in pixels.
+    int calculateBlockMargin(const QTextBlock &p_block);
+
+    VMdEditor *m_editor;
+
+    VDownloader *m_downloader;
+
+    // Whether preview is enabled.
+    bool m_previewEnabled;
+
+    // Regions of all the image links.
+    QVector<VElementRegion> m_imageRegions;
+
+    // All preview images and information.
+    // Each preview source corresponds to one vector.
+    QVector<QVector<VBlockImageInfo>> m_blockImageInfo;
+
+    // Map from URL to name in the resource manager.
+    // Used for downloading images.
+    QHash<QString, QString> m_urlToName;
+};
+
+#endif // VPREVIEWMANAGER_H