Browse Source

support normal Vim mode

1. Support Insert/Normal/Visual/VisualLine modes:
    - `V`, `v`, `I`, `i`, `A`, `a`, `s`, `Esc`, `Ctrl+[`, `o`, `O`;
2. Support movement commands (with Repeat support):
    - `h`, `j`, `k`, `l`, `gj`, `gk`;
    - `gg`, `G`;
    - `^`, `0`, `$`;
    - `Ctrl+U`, `Ctrl+D`, `PageUp`, `PageDown`, `Ctrl+B`;
Le Tan 8 years ago
parent
commit
a8c76d6742

+ 4 - 1
src/resources/styles/default.mdhl

@@ -14,7 +14,10 @@ background: 005fff
 
 editor-current-line
 background: c5cae9
-vim-background: a5d6a7
+vim-insert-background: c5cae9
+vim-normal-background: a5d6a7
+vim-visual-background: a5d6a7
+vim-replace-background: a5d6a7
 
 H1
 foreground: 111111

+ 3 - 0
src/resources/vnote.ini

@@ -45,6 +45,9 @@ image_folder=_v_images
 ; Enable trailing space highlight
 enable_trailing_space_highlight=true
 
+; Enable Vim mode in edit mode
+enable_vim_mode=false
+
 [session]
 tools_dock_checked=true
 

+ 4 - 2
src/src.pro

@@ -64,7 +64,8 @@ SOURCES += main.cpp\
     vimagepreviewer.cpp \
     vexporter.cpp \
     vmdtab.cpp \
-    vhtmltab.cpp
+    vhtmltab.cpp \
+    utils/vvim.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -115,7 +116,8 @@ HEADERS  += vmainwindow.h \
     vimagepreviewer.h \
     vexporter.h \
     vmdtab.h \
-    vhtmltab.h
+    vhtmltab.h \
+    utils/vvim.h
 
 RESOURCES += \
     vnote.qrc \

+ 1010 - 0
src/utils/vvim.cpp

@@ -0,0 +1,1010 @@
+#include "vvim.h"
+#include <QKeyEvent>
+#include <QTextBlock>
+#include <QTextDocument>
+#include <QString>
+#include <QScrollBar>
+#include <QDebug>
+#include "vedit.h"
+
+VVim::VVim(VEdit *p_editor)
+    : QObject(p_editor), m_editor(p_editor),
+      m_editConfig(&p_editor->getConfig()), m_mode(VimMode::Normal),
+      m_resetPositionInBlock(true)
+{
+    connect(m_editor, &VEdit::copyAvailable,
+            this, &VVim::selectionToVisualMode);
+}
+
+// Set @p_cursor's position specified by @p_positionInBlock.
+// If @p_positionInBlock is bigger than the block's length, move to the end of block.
+// Need to setTextCursor() after calling this.
+static void setCursorPositionInBlock(QTextCursor &p_cursor, int p_positionInBlock,
+                                     QTextCursor::MoveMode p_mode)
+{
+    QTextBlock block = p_cursor.block();
+    if (block.length() > p_positionInBlock) {
+        p_cursor.setPosition(block.position() + p_positionInBlock, p_mode);
+    } else {
+        p_cursor.movePosition(QTextCursor::EndOfBlock, p_mode, 1);
+    }
+}
+
+// Move @p_cursor to the first non-space character of current block.
+// Need to setTextCursor() after calling this.
+static void moveCursorFirstNonSpaceCharacter(QTextCursor &p_cursor,
+                                             QTextCursor::MoveMode p_mode)
+{
+    QTextBlock block = p_cursor.block();
+    QString text = block.text();
+    int idx = 0;
+    for (; idx < text.size(); ++idx) {
+        if (text[idx].isSpace()) {
+            continue;
+        } else {
+            break;
+        }
+    }
+
+    p_cursor.setPosition(block.position() + idx, p_mode);
+}
+
+bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
+{
+    bool ret = false;
+    int modifiers = p_event->modifiers();
+    int key = p_event->key();
+    bool resetPositionInBlock = true;
+    Key keyInfo(key, modifiers);
+
+    // Handle Insert mode key press.
+    if (VimMode::Insert == m_mode) {
+        if (key == Qt::Key_Escape
+            || (key == Qt::Key_BracketLeft && modifiers == Qt::ControlModifier)) {
+            // Clear selection and enter Normal mode.
+            clearSelection();
+
+            setMode(VimMode::Normal);
+            goto clear_accept;
+        }
+
+        // Let it be handled outside VVim.
+        goto exit;
+    }
+
+    // We will add key to m_keys. If all m_keys can combined to a token, add
+    // a new token to m_tokens, clear m_keys and try to process m_tokens.
+    switch (key) {
+    // Ctrl and Shift may be sent out first.
+    case Qt::Key_Control:
+        // Fall through.
+    case Qt::Key_Shift:
+    {
+        goto accept;
+    }
+
+    case Qt::Key_0:
+    {
+        if (modifiers == Qt::NoModifier) {
+            if (!m_keys.isEmpty()) {
+                // Repeat.
+                V_ASSERT(m_keys.last().isDigit());
+
+                m_keys.append(keyInfo);
+                resetPositionInBlock = false;
+                goto accept;
+            } else {
+                // StartOfLine.
+                if (m_tokens.isEmpty()) {
+                    // Move.
+                    m_tokens.append(Token(Action::Move));
+                }
+
+                m_tokens.append(Token(Movement::StartOfLine));
+
+                processCommand(m_tokens);
+            }
+        }
+
+        break;
+    }
+
+    case Qt::Key_1:
+    case Qt::Key_2:
+    case Qt::Key_3:
+    case Qt::Key_4:
+    case Qt::Key_5:
+    case Qt::Key_6:
+    case Qt::Key_7:
+    case Qt::Key_8:
+    case Qt::Key_9:
+    {
+        if (modifiers == Qt::NoModifier) {
+            if (!m_keys.isEmpty() && numberFromKeySequence(m_keys) == -1) {
+                // Invalid sequence.
+                break;
+            }
+
+            m_keys.append(keyInfo);
+            resetPositionInBlock = false;
+            goto accept;
+        }
+
+        break;
+    }
+
+    case Qt::Key_Left:
+    case Qt::Key_Down:
+    case Qt::Key_Up:
+    case Qt::Key_Right:
+    case Qt::Key_H:
+    case Qt::Key_J:
+    case Qt::Key_K:
+    case Qt::Key_L:
+    {
+        if (modifiers == Qt::NoModifier) {
+            // Check if we could generate a Repeat token.
+            tryGetRepeatToken(m_keys, m_tokens);
+
+            // Generate a Movement token.
+            Movement mm = Movement::Invalid;
+
+            if (!m_keys.isEmpty()) {
+                // gj, gk.
+                Key gKey(Qt::Key_G);
+                if (m_keys.size() == 1 && m_keys.at(0) == gKey) {
+                    if (key == Qt::Key_J) {
+                        mm = Movement::VisualDown;
+                    } else if (key == Qt::Key_K) {
+                        mm = Movement::VisualUp;
+                    } else {
+                        break;
+                    }
+                } else {
+                    // Not a valid sequence.
+                    break;
+                }
+            } else {
+                // h, j, k, l.
+                switch (key) {
+                case Qt::Key_H:
+                case Qt::Key_Left:
+                    mm = Movement::Left;
+                    break;
+
+                case Qt::Key_L:
+                case Qt::Key_Right:
+                    mm = Movement::Right;
+                    break;
+
+                case Qt::Key_J:
+                case Qt::Key_Down:
+                    mm = Movement::Down;
+                    break;
+
+                case Qt::Key_K:
+                case Qt::Key_Up:
+                    mm = Movement::Up;
+                    break;
+
+                default:
+                    V_ASSERT(false);
+                }
+            }
+
+            V_ASSERT(mm != Movement::Invalid);
+            if (m_tokens.isEmpty()) {
+                // Move.
+                m_tokens.append(Token(Action::Move));
+            }
+
+            m_tokens.append(Token(mm));
+            processCommand(m_tokens);
+            resetPositionInBlock = false;
+        }
+
+        break;
+    }
+
+    case Qt::Key_I:
+    {
+        if (modifiers == Qt::NoModifier) {
+            // Enter Insert mode.
+            if (m_mode == VimMode::Normal) {
+                setMode(VimMode::Insert);
+            }
+        } else if (modifiers == Qt::ShiftModifier) {
+            QTextCursor cursor = m_editor->textCursor();
+            if (m_mode == VimMode::Normal) {
+                // Insert at the first non-space character.
+                moveCursorFirstNonSpaceCharacter(cursor, QTextCursor::MoveAnchor);
+            } else if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) {
+                // Insert at the start of line.
+                cursor.movePosition(QTextCursor::StartOfBlock,
+                                    QTextCursor::MoveAnchor,
+                                    1);
+            }
+
+            m_editor->setTextCursor(cursor);
+            setMode(VimMode::Insert);
+        }
+
+        break;
+    }
+
+    case Qt::Key_A:
+    {
+        if (modifiers == Qt::NoModifier) {
+            // Enter Insert mode.
+            // Move cursor back one character.
+            if (m_mode == VimMode::Normal) {
+                QTextCursor cursor = m_editor->textCursor();
+                V_ASSERT(!cursor.hasSelection());
+
+                if (!cursor.atBlockEnd()) {
+                    cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 1);
+                    m_editor->setTextCursor(cursor);
+                }
+
+                setMode(VimMode::Insert);
+            }
+        } else if (modifiers == Qt::ShiftModifier) {
+            // Insert at the end of line.
+            QTextCursor cursor = m_editor->textCursor();
+            if (m_mode == VimMode::Normal) {
+                cursor.movePosition(QTextCursor::EndOfBlock,
+                                    QTextCursor::MoveAnchor,
+                                    1);
+                m_editor->setTextCursor(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);
+                }
+            }
+
+            setMode(VimMode::Insert);
+        }
+
+        break;
+    }
+
+    case Qt::Key_O:
+    {
+        if (modifiers == Qt::NoModifier) {
+            // Insert a new block under current block and enter insert mode.
+            if (m_mode == VimMode::Normal) {
+                QTextCursor cursor = m_editor->textCursor();
+                cursor.movePosition(QTextCursor::EndOfBlock,
+                                    QTextCursor::MoveAnchor,
+                                    1);
+                cursor.insertBlock();
+                m_editor->setTextCursor(cursor);
+                setMode(VimMode::Insert);
+            }
+        } else if (modifiers == Qt::ShiftModifier) {
+            // Insert a new block above current block and enter insert mode.
+            if (m_mode == VimMode::Normal) {
+                QTextCursor cursor = m_editor->textCursor();
+                cursor.movePosition(QTextCursor::StartOfBlock,
+                                    QTextCursor::MoveAnchor,
+                                    1);
+                cursor.insertBlock();
+                cursor.movePosition(QTextCursor::PreviousBlock,
+                                    QTextCursor::MoveAnchor,
+                                    1);
+                m_editor->setTextCursor(cursor);
+                setMode(VimMode::Insert);
+            }
+        }
+
+        break;
+    }
+
+    case Qt::Key_S:
+    {
+        if (modifiers == Qt::NoModifier) {
+            // 1. If there is selection, delete the selected text.
+            // 2. Otherwise, if cursor is not at the end of block, delete the
+            //    character after current cursor.
+            // 3. Enter Insert mode.
+            QTextCursor cursor = m_editor->textCursor();
+            if (cursor.hasSelection() || !cursor.atBlockEnd()) {
+                cursor.deleteChar();
+                m_editor->setTextCursor(cursor);
+            }
+
+            setMode(VimMode::Insert);
+        }
+
+        break;
+    }
+
+    case Qt::Key_Dollar:
+    {
+        if (modifiers == Qt::ShiftModifier) {
+            // $, move to end of line.
+            tryGetRepeatToken(m_keys, m_tokens);
+
+            if (m_keys.isEmpty()) {
+                if (m_tokens.isEmpty()) {
+                    // Move.
+                    m_tokens.append(Token(Action::Move));
+                }
+
+                m_tokens.append(Token(Movement::EndOfLine));
+                processCommand(m_tokens);
+            }
+        }
+
+        break;
+    }
+
+    case Qt::Key_G:
+    {
+        Movement mm = Movement::Invalid;
+        if (modifiers == Qt::NoModifier) {
+            tryGetRepeatToken(m_keys, m_tokens);
+            if (m_keys.isEmpty()) {
+                // First g, pend it.
+                m_keys.append(keyInfo);
+                goto accept;
+            } else if (m_keys.size() == 1 && m_keys.at(0) == keyInfo) {
+                // gg, go to a certain line or first line.
+                if (!m_tokens.isEmpty() && m_tokens.last().isRepeat()) {
+                    mm = Movement::LineJump;
+                } else {
+                    mm = Movement::StartOfDocument;
+                }
+            }
+        } else if (modifiers == Qt::ShiftModifier) {
+            tryGetRepeatToken(m_keys, m_tokens);
+            if (m_keys.isEmpty()) {
+                // G, go to a certain line or the last line.
+                if (!m_tokens.isEmpty() && m_tokens.last().isRepeat()) {
+                    mm = Movement::LineJump;
+                } else {
+                    mm = Movement::EndOfDocument;
+                }
+            }
+        }
+
+        if (mm != Movement::Invalid) {
+            if (m_tokens.isEmpty()) {
+                // Move.
+                m_tokens.append(Token(Action::Move));
+            }
+
+            m_tokens.append(Token(mm));
+            processCommand(m_tokens);
+        }
+
+        break;
+    }
+
+    // Should be kept together with Qt::Key_PageUp.
+    case Qt::Key_B:
+    {
+        if (modifiers == Qt::ControlModifier) {
+            // Ctrl+B, page up, fall through.
+            modifiers = Qt::NoModifier;
+        } else {
+            break;
+        }
+    }
+
+    case Qt::Key_PageUp:
+    {
+        if (modifiers == Qt::NoModifier) {
+            tryGetRepeatToken(m_keys, m_tokens);
+            if (!m_keys.isEmpty()) {
+                // Not a valid sequence.
+                break;
+            }
+
+            Movement mm = Movement::PageUp;
+            if (m_tokens.isEmpty()) {
+                // Move.
+                m_tokens.append(Token(Action::Move));
+            }
+
+            m_tokens.append(Token(mm));
+            processCommand(m_tokens);
+            resetPositionInBlock = false;
+        }
+
+        break;
+    }
+
+    case Qt::Key_U:
+    {
+        if (modifiers == Qt::ControlModifier) {
+            // Ctrl+U, HalfPageUp.
+            tryGetRepeatToken(m_keys, m_tokens);
+            if (!m_keys.isEmpty()) {
+                // Not a valid sequence.
+                break;
+            }
+
+            Movement mm = Movement::HalfPageUp;
+            if (m_tokens.isEmpty()) {
+                // Move.
+                m_tokens.append(Token(Action::Move));
+            }
+
+            m_tokens.append(Token(mm));
+            processCommand(m_tokens);
+            resetPositionInBlock = false;
+        }
+
+        break;
+    }
+
+    // Ctrl+F is used for Find dialog, not used here.
+    case Qt::Key_PageDown:
+    {
+        if (modifiers == Qt::NoModifier) {
+            tryGetRepeatToken(m_keys, m_tokens);
+            if (!m_keys.isEmpty()) {
+                // Not a valid sequence.
+                break;
+            }
+
+            Movement mm = Movement::PageDown;
+            if (m_tokens.isEmpty()) {
+                // Move.
+                m_tokens.append(Token(Action::Move));
+            }
+
+            m_tokens.append(Token(mm));
+            processCommand(m_tokens);
+            resetPositionInBlock = false;
+        }
+
+        break;
+    }
+
+    case Qt::Key_D:
+    {
+        if (modifiers == Qt::ControlModifier) {
+            // Ctrl+D, HalfPageDown.
+            tryGetRepeatToken(m_keys, m_tokens);
+            if (!m_keys.isEmpty()) {
+                // Not a valid sequence.
+                break;
+            }
+
+            Movement mm = Movement::HalfPageDown;
+            if (m_tokens.isEmpty()) {
+                // Move.
+                m_tokens.append(Token(Action::Move));
+            }
+
+            m_tokens.append(Token(mm));
+            processCommand(m_tokens);
+            resetPositionInBlock = false;
+        }
+
+        break;
+    }
+
+    // Should be kept together with Qt::Key_Escape.
+    case Qt::Key_BracketLeft:
+    {
+        if (modifiers == Qt::ControlModifier) {
+            // fallthrough.
+        } else {
+            break;
+        }
+
+    }
+
+    case Qt::Key_Escape:
+    {
+        // Clear selection and enter normal mode.
+        clearSelection();
+
+        setMode(VimMode::Normal);
+        break;
+    }
+
+    case Qt::Key_V:
+    {
+        if (modifiers == Qt::NoModifier) {
+            // Toggle Visual Mode.
+            clearSelection();
+            VimMode mode = VimMode::Visual;
+            if (m_mode == VimMode::Visual) {
+                mode = VimMode::Normal;
+            }
+
+            setMode(mode);
+        } else if (modifiers == Qt::ShiftModifier) {
+            // Visual Line Mode.
+            clearSelection();
+            VimMode mode = VimMode::VisualLine;
+            if (m_mode == VimMode::VisualLine) {
+                mode = VimMode::Normal;
+            }
+
+            setMode(mode);
+
+            if (m_mode == VimMode::VisualLine) {
+                QTextCursor cursor = m_editor->textCursor();
+                expandSelectionInVisualLineMode(cursor);
+                m_editor->setTextCursor(cursor);
+            }
+        }
+
+        break;
+    }
+
+    case Qt::Key_AsciiCircum:
+    {
+        if (modifiers == Qt::ShiftModifier) {
+            // ~, go to first non-space character of current line (block).
+            tryGetRepeatToken(m_keys, m_tokens);
+            if (!m_keys.isEmpty()) {
+                // Not a valid sequence.
+                break;
+            }
+
+            Movement mm = Movement::FirstCharacter;
+            if (m_tokens.isEmpty()) {
+                // Move.
+                m_tokens.append(Token(Action::Move));
+            }
+
+            m_tokens.append(Token(mm));
+            processCommand(m_tokens);
+        }
+
+        break;
+    }
+
+    default:
+        break;
+    }
+
+clear_accept:
+    m_keys.clear();
+    m_tokens.clear();
+
+accept:
+    p_event->accept();
+    ret = true;
+
+exit:
+    m_resetPositionInBlock = resetPositionInBlock;
+    return ret;
+}
+
+void VVim::resetState()
+{
+    m_keys.clear();
+    m_tokens.clear();
+    m_resetPositionInBlock = true;
+}
+
+VimMode VVim::getMode() const
+{
+    return m_mode;
+}
+
+void VVim::setMode(VimMode p_mode)
+{
+    if (m_mode != p_mode) {
+        m_mode = p_mode;
+        resetState();
+
+        emit modeChanged(m_mode);
+    }
+}
+
+void VVim::processCommand(QList<Token> &p_tokens)
+{
+    if (p_tokens.isEmpty()) {
+        return;
+    }
+
+    V_ASSERT(p_tokens.at(0).isAction());
+
+    qDebug() << "process tokens of size" << p_tokens.size();
+    for (int i = 0; i < p_tokens.size(); ++i) {
+        qDebug() << "token" << i << p_tokens[i].toString();
+    }
+
+    Token act = p_tokens.takeFirst();
+    switch (act.m_action) {
+    case Action::Move:
+        processMoveAction(p_tokens);
+        break;
+
+    default:
+        p_tokens.clear();
+        break;
+    }
+
+    Q_ASSERT(p_tokens.isEmpty());
+}
+
+int VVim::numberFromKeySequence(const QList<Key> &p_keys)
+{
+    int num = 0;
+
+    for (auto const & key : p_keys) {
+        if (key.isDigit()) {
+            num = num * 10 + key.toDigit();
+        } else {
+            return -1;
+        }
+    }
+
+    return num == 0 ? -1 : num;
+}
+
+bool VVim::tryGetRepeatToken(QList<Key> &p_keys, QList<Token> &p_tokens)
+{
+    if (!p_keys.isEmpty()) {
+        int repeat = numberFromKeySequence(p_keys);
+        if (repeat != -1) {
+            if (p_tokens.isEmpty()) {
+                // Move.
+                p_tokens.append(Token(Action::Move));
+            }
+
+            p_tokens.append(Token(repeat));
+            p_keys.clear();
+
+            return true;
+        }
+    }
+
+    return false;
+}
+
+void VVim::processMoveAction(QList<Token> &p_tokens)
+{
+    // Only moving left/right could change this.
+    static int positionInBlock = 0;
+
+    Token to = p_tokens.takeFirst();
+    V_ASSERT(to.isRepeat() || to.isMovement());
+    Token mvToken;
+    int repeat = -1;
+    if (to.isRepeat()) {
+        repeat = to.m_repeat;
+        mvToken = p_tokens.takeFirst();
+    } else {
+        mvToken = to;
+    }
+
+    if (!mvToken.isMovement() || !p_tokens.isEmpty()) {
+        p_tokens.clear();
+        return;
+    }
+
+    QTextCursor cursor = m_editor->textCursor();
+    QTextDocument *doc = m_editor->document();
+    if (m_resetPositionInBlock) {
+        positionInBlock = cursor.positionInBlock();
+    }
+
+    bool hasMoved = false;
+    QTextCursor::MoveMode moveMode = (m_mode == VimMode::Visual
+                                      || m_mode == VimMode::VisualLine)
+                                     ? QTextCursor::KeepAnchor
+                                     : QTextCursor::MoveAnchor;
+    switch (mvToken.m_movement) {
+    case Movement::Left:
+    {
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        int pib = cursor.positionInBlock();
+        repeat = qMin(pib, repeat);
+
+        if (repeat > 0) {
+            cursor.movePosition(QTextCursor::Left, moveMode, repeat);
+            positionInBlock = cursor.positionInBlock();
+            hasMoved = true;
+        }
+
+        break;
+    }
+
+    case Movement::Right:
+    {
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        int pib = cursor.positionInBlock();
+        int length = cursor.block().length();
+        if (length - pib <= repeat) {
+            repeat = length - pib - 1;
+        }
+
+        if (repeat > 0) {
+            cursor.movePosition(QTextCursor::Right, moveMode, repeat);
+            positionInBlock = cursor.positionInBlock();
+            hasMoved = true;
+        }
+
+        break;
+    }
+
+    case Movement::Up:
+    {
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        repeat = qMin(cursor.block().blockNumber(), repeat);
+
+        if (repeat > 0) {
+            cursor.movePosition(QTextCursor::PreviousBlock, moveMode, repeat);
+            if (positionInBlock > 0) {
+                setCursorPositionInBlock(cursor, positionInBlock, moveMode);
+            }
+
+            hasMoved = true;
+        }
+
+        break;
+    }
+
+    case Movement::Down:
+    {
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        int blockCount = m_editor->document()->blockCount();
+        repeat = qMin(blockCount - 1 - cursor.block().blockNumber(), repeat);
+
+        if (repeat > 0) {
+            cursor.movePosition(QTextCursor::NextBlock, moveMode, repeat);
+            if (positionInBlock > 0) {
+                setCursorPositionInBlock(cursor, positionInBlock, moveMode);
+            }
+
+            hasMoved = true;
+        }
+
+        break;
+    }
+
+    case Movement::VisualUp:
+    {
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        cursor.movePosition(QTextCursor::Up, moveMode, repeat);
+        hasMoved = true;
+        break;
+    }
+
+    case Movement::VisualDown:
+    {
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        cursor.movePosition(QTextCursor::Down, moveMode, repeat);
+        hasMoved = true;
+        break;
+    }
+
+    case Movement::PageUp:
+    {
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        int blockStep = blockCountOfPageStep() * repeat;
+        int block = cursor.block().blockNumber();
+        block = qMax(0, block - blockStep);
+        cursor.setPosition(doc->findBlockByNumber(block).position(), moveMode);
+
+        setCursorPositionInBlock(cursor, positionInBlock, moveMode);
+        hasMoved = true;
+
+        break;
+    }
+
+    case Movement::PageDown:
+    {
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        int blockStep = blockCountOfPageStep() * repeat;
+        int block = cursor.block().blockNumber();
+        block = qMin(block + blockStep, doc->blockCount() - 1);
+        cursor.setPosition(doc->findBlockByNumber(block).position(), moveMode);
+
+        setCursorPositionInBlock(cursor, positionInBlock, moveMode);
+        hasMoved = true;
+
+        break;
+    }
+
+    case Movement::HalfPageUp:
+    {
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        int blockStep = blockCountOfPageStep();
+        int halfBlockStep = qMax(blockStep / 2, 1);
+        blockStep = repeat * halfBlockStep;
+        int block = cursor.block().blockNumber();
+        block = qMax(0, block - blockStep);
+        cursor.setPosition(doc->findBlockByNumber(block).position(), moveMode);
+
+        setCursorPositionInBlock(cursor, positionInBlock, moveMode);
+        hasMoved = true;
+
+        break;
+    }
+
+    case Movement::HalfPageDown:
+    {
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        int blockStep = blockCountOfPageStep();
+        int halfBlockStep = qMax(blockStep / 2, 1);
+        blockStep = repeat * halfBlockStep;
+        int block = cursor.block().blockNumber();
+        block = qMin(block + blockStep, doc->blockCount() - 1);
+        cursor.setPosition(doc->findBlockByNumber(block).position(), moveMode);
+
+        setCursorPositionInBlock(cursor, positionInBlock, moveMode);
+        hasMoved = true;
+
+        break;
+    }
+
+    case Movement::StartOfLine:
+    {
+        Q_ASSERT(repeat == -1);
+
+        // Start of the Line (block).
+        cursor.movePosition(QTextCursor::StartOfBlock, moveMode, 1);
+        hasMoved = true;
+        break;
+    }
+
+    case Movement::EndOfLine:
+    {
+        // End of line (block).
+        if (repeat == -1) {
+            repeat = 1;
+        } else if (repeat > 1) {
+            // Move down (repeat-1) blocks.
+            cursor.movePosition(QTextCursor::NextBlock, moveMode, repeat - 1);
+        }
+
+        // Move to the end of block.
+        cursor.movePosition(QTextCursor::EndOfBlock, moveMode, 1);
+        hasMoved = true;
+        break;
+    }
+
+    case Movement::FirstCharacter:
+    {
+        // repeat is not considered in this command.
+        // If all the block is space, just move to the end of block; otherwise,
+        // move to the first non-space character.
+        moveCursorFirstNonSpaceCharacter(cursor, moveMode);
+        hasMoved = true;
+        break;
+    }
+
+    case Movement::LineJump:
+    {
+        // Jump to the first non-space character of @repeat line (block).
+        V_ASSERT(repeat > 0);
+
+        // @repeat starts from 1 while block number starts from 0.
+        QTextBlock block = doc->findBlockByNumber(repeat - 1);
+        if (block.isValid()) {
+            cursor.setPosition(block.position(), moveMode);
+        } else {
+            // Go beyond the document.
+            cursor.movePosition(QTextCursor::End, moveMode, 1);
+        }
+
+        moveCursorFirstNonSpaceCharacter(cursor, moveMode);
+        hasMoved = true;
+        break;
+    }
+
+    case Movement::StartOfDocument:
+    {
+        // Jump to the first non-space character of the start of the document.
+        V_ASSERT(repeat == -1);
+        cursor.movePosition(QTextCursor::Start, moveMode, 1);
+        moveCursorFirstNonSpaceCharacter(cursor, moveMode);
+        hasMoved = true;
+        break;
+    }
+
+    case Movement::EndOfDocument:
+    {
+        // Jump to the first non-space character of the end of the document.
+        V_ASSERT(repeat == -1);
+        cursor.movePosition(QTextCursor::End, moveMode, 1);
+        moveCursorFirstNonSpaceCharacter(cursor, moveMode);
+        hasMoved = true;
+        break;
+    }
+
+    default:
+        break;
+    }
+
+    if (hasMoved) {
+        expandSelectionInVisualLineMode(cursor);
+        m_editor->setTextCursor(cursor);
+    }
+}
+
+bool VVim::clearSelection()
+{
+    QTextCursor cursor = m_editor->textCursor();
+    if (cursor.hasSelection()) {
+        cursor.clearSelection();
+        m_editor->setTextCursor(cursor);
+        return true;
+    }
+
+    return false;
+}
+
+int VVim::blockCountOfPageStep() const
+{
+    int lineCount = m_editor->document()->blockCount();
+    QScrollBar *bar = m_editor->verticalScrollBar();
+    int steps = (bar->maximum() - bar->minimum() + bar->pageStep());
+    int pageLineCount = lineCount * (bar->pageStep() * 1.0 / steps);
+    return pageLineCount;
+}
+
+void VVim::selectionToVisualMode(bool p_hasText)
+{
+    if (p_hasText && m_mode == VimMode::Normal) {
+        // Enter visual mode.
+        setMode(VimMode::Visual);
+    }
+}
+
+void VVim::expandSelectionInVisualLineMode(QTextCursor &p_cursor)
+{
+    if (m_mode != VimMode::VisualLine) {
+        return;
+    }
+
+    QTextDocument *doc = m_editor->document();
+    int curPos = p_cursor.position();
+    int anchorPos = p_cursor.anchor();
+    QTextBlock curBlock = doc->findBlock(curPos);
+    QTextBlock anchorBlock = doc->findBlock(anchorPos);
+
+    if (curPos >= anchorPos) {
+        p_cursor.setPosition(anchorBlock.position(), QTextCursor::MoveAnchor);
+        p_cursor.setPosition(curBlock.position() + curBlock.length() - 1,
+                             QTextCursor::KeepAnchor);
+    } else {
+        p_cursor.setPosition(anchorBlock.position() + anchorBlock.length() - 1,
+                             QTextCursor::MoveAnchor);
+        p_cursor.setPosition(curBlock.position(),
+                             QTextCursor::KeepAnchor);
+    }
+}

+ 243 - 0
src/utils/vvim.h

@@ -0,0 +1,243 @@
+#ifndef VVIM_H
+#define VVIM_H
+
+#include <QObject>
+#include <QString>
+#include <QTextCursor>
+#include "vutils.h"
+
+class VEdit;
+class QKeyEvent;
+class VEditConfig;
+class QKeyEvent;
+
+enum class VimMode {
+    Normal = 0,
+    Insert,
+    Visual,
+    VisualLine,
+    Replace,
+    Invalid
+};
+
+class VVim : public QObject
+{
+    Q_OBJECT
+public:
+    explicit VVim(VEdit *p_editor);
+
+    // Handle key press event.
+    // Returns true if the event is consumed and need no more handling.
+    bool handleKeyPressEvent(QKeyEvent *p_event);
+
+    // Return current mode.
+    VimMode getMode() const;
+
+    // Set current mode.
+    void setMode(VimMode p_mode);
+
+signals:
+    // Emit when current mode has been changed.
+    void modeChanged(VimMode p_mode);
+
+private slots:
+    // When user use mouse to select texts in Normal mode, we should change to
+    // Visual mode.
+    void selectionToVisualMode(bool p_hasText);
+
+private:
+    // Struct for a key press.
+    struct Key
+    {
+        Key(int p_key, int p_modifiers = Qt::NoModifier)
+            : m_key(p_key), m_modifiers(p_modifiers)
+        {
+        }
+
+        int m_key;
+        int m_modifiers;
+
+        bool isDigit() const
+        {
+            return m_key >= Qt::Key_0
+                   && m_key <= Qt::Key_9
+                   && m_modifiers == Qt::NoModifier;
+        }
+
+        int toDigit() const
+        {
+            V_ASSERT(isDigit());
+            return m_key - Qt::Key_0;
+        }
+
+        bool operator==(const Key &p_key) const
+        {
+            return p_key.m_key == m_key && p_key.m_modifiers == m_modifiers;
+        }
+    };
+
+    // Supported actions.
+    enum class Action
+    {
+        Move = 0,
+        Delete,
+        Copy,
+        Paste,
+        Change,
+        Indent,
+        UnIndent,
+        ToUpper,
+        ToLower,
+        DeleteToClipboard,
+        CopyToClipboard,
+        PasteFromClipboard,
+        ChangeToClipboard,
+        Invalid
+    };
+
+    // Supported movements.
+    enum class Movement
+    {
+        Left = 0,
+        Right,
+        Up,
+        Down,
+        VisualUp,
+        VisualDown,
+        PageUp,
+        PageDown,
+        HalfPageUp,
+        HalfPageDown,
+        StartOfLine,
+        EndOfLine,
+        FirstCharacter,
+        LineJump,
+        StartOfDocument,
+        EndOfDocument,
+        Invalid
+    };
+
+    // Supported ranges.
+    enum class Range
+    {
+        Line = 0,
+        Word,
+        Invalid
+    };
+
+    enum class TokenType { Action = 0, Repeat, Movement, Range, Invalid };
+
+    struct Token
+    {
+        Token(Action p_action)
+            : m_type(TokenType::Action), m_action(p_action) {}
+
+        Token(int p_repeat)
+            : m_type(TokenType::Repeat), m_repeat(p_repeat) {}
+
+        Token(Movement p_movement)
+            : m_type(TokenType::Movement), m_movement(p_movement) {}
+
+        Token(Range p_range)
+            : m_type(TokenType::Range), m_range(p_range) {}
+
+        Token() : m_type(TokenType::Invalid) {}
+
+        bool isRepeat() const
+        {
+            return m_type == TokenType::Repeat;
+        }
+
+        bool isAction() const
+        {
+            return m_type == TokenType::Action;
+        }
+
+        bool isMovement() const
+        {
+            return m_type == TokenType::Movement;
+        }
+
+        bool isRange() const
+        {
+            return m_type == TokenType::Range;
+        }
+
+        QString toString() const
+        {
+            QString str;
+            switch (m_type) {
+            case TokenType::Action:
+                str = QString("action %1").arg((int)m_action);
+                break;
+
+            case TokenType::Repeat:
+                str = QString("repeat %1").arg(m_repeat);
+                break;
+
+            case TokenType::Movement:
+                str = QString("movement %1").arg((int)m_movement);
+                break;
+
+            case TokenType::Range:
+                str = QString("range %1").arg((int)m_range);
+                break;
+
+            default:
+                str = "invalid";
+            }
+
+            return str;
+        }
+
+        TokenType m_type;
+
+        union
+        {
+            Action m_action;
+            int m_repeat;
+            Movement m_movement;
+            Range m_range;
+        };
+    };
+
+    // Reset all key state info.
+    void resetState();
+
+    // Now m_tokens constitute a command. Execute it.
+    // Will clear @p_tokens.
+    void processCommand(QList<Token> &p_tokens);
+
+    // Return the number represented by @p_keys.
+    // Return -1 if @p_keys is not a valid digit sequence.
+    int numberFromKeySequence(const QList<Key> &p_keys);
+
+    // Try to generate a Repeat token from @p_keys and insert it to @p_tokens.
+    // If succeed, clear @p_keys and return true.
+    bool tryGetRepeatToken(QList<Key> &p_keys, QList<Token> &p_tokens);
+
+    // @p_tokens is the arguments of the Action::Move action.
+    void processMoveAction(QList<Token> &p_tokens);
+
+    // Clear selection if there is any.
+    // Returns true if there is selection.
+    bool clearSelection();
+
+    // Get the block count of one page step in vertical scroll bar.
+    int blockCountOfPageStep() const;
+
+    // Expand selection in the VisualLiine mode which will change the position
+    // of @p_cursor.
+    void expandSelectionInVisualLineMode(QTextCursor &p_cursor);
+
+    VEdit *m_editor;
+    const VEditConfig *m_editConfig;
+    VimMode m_mode;
+    QList<Key> m_keys;
+    QList<Token> m_tokens;
+
+    // Whether reset the position in block when moving cursor.
+    bool m_resetPositionInBlock;
+};
+
+#endif // VVIM_H

+ 29 - 12
src/vconfigmanager.cpp

@@ -146,6 +146,9 @@ void VConfigManager::initialize()
 
     m_enableTrailingSpaceHighlight = getConfigFromSettings("global",
                                                            "enable_trailing_space_highlight").toBool();
+
+    m_enableVimMode = getConfigFromSettings("global",
+                                            "enable_vim_mode").toBool();
 }
 
 void VConfigManager::readPredefinedColorsFromSettings()
@@ -347,34 +350,48 @@ void VConfigManager::updateMarkdownEditStyle()
     QMap<QString, QMap<QString, QString>> styles;
     parser.fetchMarkdownEditorStyles(mdEditPalette, mdEditFont, styles);
 
-    m_editorCurrentLineBackground = defaultCurrentLineBackground;
-    m_editorCurrentLineVimBackground = defaultCurrentLineVimBackground;
+    m_editorCurrentLineBg = defaultCurrentLineBackground;
+    m_editorVimInsertBg = defaultCurrentLineBackground;
+    m_editorVimNormalBg = defaultCurrentLineVimBackground;
+    m_editorVimVisualBg = m_editorVimNormalBg;
+    m_editorVimReplaceBg = m_editorVimNormalBg;
     auto editorCurrentLineIt = styles.find("editor-current-line");
     if (editorCurrentLineIt != styles.end()) {
         auto backgroundIt = editorCurrentLineIt->find("background");
         if (backgroundIt != editorCurrentLineIt->end()) {
             // Do not need to add "#" here, since this is a built-in attribute.
-            m_editorCurrentLineBackground = *backgroundIt;
+            m_editorCurrentLineBg = *backgroundIt;
+        }
+
+        auto vimBgIt = editorCurrentLineIt->find("vim-insert-background");
+        if (vimBgIt != editorCurrentLineIt->end()) {
+            m_editorVimInsertBg = "#" + *vimBgIt;
+        }
+
+        vimBgIt = editorCurrentLineIt->find("vim-normal-background");
+        if (vimBgIt != editorCurrentLineIt->end()) {
+            m_editorVimNormalBg = "#" + *vimBgIt;
+        }
+
+        vimBgIt = editorCurrentLineIt->find("vim-visual-background");
+        if (vimBgIt != editorCurrentLineIt->end()) {
+            m_editorVimVisualBg = "#" + *vimBgIt;
         }
 
-        auto vimBackgroundIt = editorCurrentLineIt->find("vim-background");
-        if (vimBackgroundIt != editorCurrentLineIt->end()) {
-            m_editorCurrentLineVimBackground = "#" + *vimBackgroundIt;
+        vimBgIt = editorCurrentLineIt->find("vim-replace-background");
+        if (vimBgIt != editorCurrentLineIt->end()) {
+            m_editorVimReplaceBg = "#" + *vimBgIt;
         }
     }
 
-    m_editorTrailingSpaceBackground = defaultTrailingSpaceBackground;
+    m_editorTrailingSpaceBg = defaultTrailingSpaceBackground;
     auto editorIt = styles.find("editor");
     if (editorIt != styles.end()) {
         auto trailingIt = editorIt->find("trailing-space");
         if (trailingIt != editorIt->end()) {
-            m_editorTrailingSpaceBackground = "#" + *trailingIt;
+            m_editorTrailingSpaceBg = "#" + *trailingIt;
         }
     }
-
-    qDebug() << "editor-current-line" << m_editorCurrentLineBackground;
-    qDebug() << "editor-current-line-vim" << m_editorCurrentLineVimBackground;
-    qDebug() << "editor-trailing-space" << m_editorTrailingSpaceBackground;
 }
 
 void VConfigManager::updateEditStyle()

+ 62 - 12
src/vconfigmanager.h

@@ -156,10 +156,14 @@ public:
     void setWebZoomFactor(qreal p_factor);
     inline bool isCustomWebZoomFactor();
 
-    inline QString getEditorCurrentLineBackground() const;
-    inline QString getEditorCurrentLineVimBackground() const;
+    inline const QString &getEditorCurrentLineBg() const;
     inline QString getEditorTrailingSpaceBackground() const;
 
+    inline const QString &getEditorVimNormalBg() const;
+    inline const QString &getEditorVimInsertBg() const;
+    inline const QString &getEditorVimVisualBg() const;
+    inline const QString &getEditorVimReplaceBg() const;
+
     inline bool getEnableCodeBlockHighlight() const;
     inline void setEnableCodeBlockHighlight(bool p_enabled);
 
@@ -183,6 +187,9 @@ public:
     inline bool getEnableTrailingSpaceHighlight() const;
     inline void setEnableTrailingSapceHighlight(bool p_enabled);
 
+    inline bool getEnableVimMode() const;
+    inline void setEnableVimMode(bool p_enabled);
+
     // Get the folder the ini file exists.
     QString getConfigFolder() const;
 
@@ -292,13 +299,22 @@ private:
     qreal m_webZoomFactor;
 
     // Current line background color in editor.
-    QString m_editorCurrentLineBackground;
+    QString m_editorCurrentLineBg;
+
+    // Current line background color in editor in Vim normal mode.
+    QString m_editorVimNormalBg;
+
+    // Current line background color in editor in Vim insert mode.
+    QString m_editorVimInsertBg;
 
-    // Current line background color in editor in Vim mode.
-    QString m_editorCurrentLineVimBackground;
+    // Current line background color in editor in Vim visual mode.
+    QString m_editorVimVisualBg;
+
+    // Current line background color in editor in Vim replace mode.
+    QString m_editorVimReplaceBg;
 
     // Trailing space background color in editor.
-    QString m_editorTrailingSpaceBackground;
+    QString m_editorTrailingSpaceBg;
 
     // Enable colde block syntax highlight.
     bool m_enableCodeBlockHighlight;
@@ -322,6 +338,9 @@ private:
     // Enable trailing-space highlight.
     bool m_enableTrailingSpaceHighlight;
 
+    // Enable Vim mode.
+    bool m_enableVimMode;
+
     // The name of the config file in each directory, obsolete.
     // Use c_dirConfigFile instead.
     static const QString c_obsoleteDirConfigFile;
@@ -722,19 +741,34 @@ inline bool VConfigManager::isCustomWebZoomFactor()
     return factorFromIni > 0;
 }
 
-inline QString VConfigManager::getEditorCurrentLineBackground() const
+inline const QString &VConfigManager::getEditorCurrentLineBg() const
 {
-    return m_editorCurrentLineBackground;
+    return m_editorCurrentLineBg;
 }
 
-inline QString VConfigManager::getEditorCurrentLineVimBackground() const
+inline QString VConfigManager::getEditorTrailingSpaceBackground() const
 {
-    return m_editorCurrentLineVimBackground;
+    return m_editorTrailingSpaceBg;
 }
 
-inline QString VConfigManager::getEditorTrailingSpaceBackground() const
+inline const QString &VConfigManager::getEditorVimNormalBg() const
+{
+    return m_editorVimNormalBg;
+}
+
+inline const QString &VConfigManager::getEditorVimInsertBg() const
+{
+    return m_editorVimInsertBg;
+}
+
+inline const QString &VConfigManager::getEditorVimVisualBg() const
+{
+    return m_editorVimVisualBg;
+}
+
+inline const QString &VConfigManager::getEditorVimReplaceBg() const
 {
-    return m_editorTrailingSpaceBackground;
+    return m_editorVimReplaceBg;
 }
 
 inline bool VConfigManager::getEnableCodeBlockHighlight() const
@@ -857,4 +891,20 @@ inline void VConfigManager::setEnableTrailingSapceHighlight(bool p_enabled)
                         m_enableTrailingSpaceHighlight);
 }
 
+inline bool VConfigManager::getEnableVimMode() const
+{
+    return m_enableVimMode;
+}
+
+inline void VConfigManager::setEnableVimMode(bool p_enabled)
+{
+    if (m_enableVimMode == p_enabled) {
+        return;
+    }
+
+    m_enableVimMode = p_enabled;
+    setConfigToSettings("global", "enable_vim_mode",
+                        m_enableVimMode);
+}
+
 #endif // VCONFIGMANAGER_H

+ 42 - 2
src/vedit.cpp

@@ -13,6 +13,27 @@
 extern VConfigManager vconfig;
 extern VNote *g_vnote;
 
+void VEditConfig::init(const QFontMetrics &p_metric)
+{
+    if (vconfig.getTabStopWidth() > 0) {
+        m_tabStopWidth = vconfig.getTabStopWidth() * p_metric.width(' ');
+    } else {
+        m_tabStopWidth = 0;
+    }
+
+    m_expandTab = vconfig.getIsExpandTab();
+
+    if (m_expandTab && (vconfig.getTabStopWidth() > 0)) {
+        m_tabSpaces = QString(vconfig.getTabStopWidth(), ' ');
+    } else {
+        m_tabSpaces = "\t";
+    }
+
+    m_enableVimMode = vconfig.getEnableVimMode();
+
+    m_cursorLineBg = QColor(vconfig.getEditorCurrentLineBg());
+}
+
 VEdit::VEdit(VFile *p_file, QWidget *p_parent)
     : QTextEdit(p_parent), m_file(p_file), m_editOps(NULL)
 {
@@ -20,7 +41,6 @@ VEdit::VEdit(VFile *p_file, QWidget *p_parent)
     const int extraSelectionHighlightTimer = 500;
     const int labelSize = 64;
 
-    m_cursorLineColor = QColor(g_vnote->getColorFromPalette("Indigo1"));
     m_selectedWordColor = QColor("Yellow");
     m_searchedWordColor = QColor(g_vnote->getColorFromPalette("Green4"));
     m_trailingSpaceColor = QColor(vconfig.getEditorTrailingSpaceBackground());
@@ -45,8 +65,11 @@ VEdit::VEdit(VFile *p_file, QWidget *p_parent)
             (VFile *)m_file, &VFile::setModified);
 
     m_extraSelections.resize((int)SelectionId::MaxSelection);
+
     updateFontAndPalette();
 
+    updateConfig();
+
     connect(this, &VEdit::cursorPositionChanged,
             this, &VEdit::handleCursorPositionChanged);
 
@@ -62,10 +85,23 @@ VEdit::~VEdit()
     }
 }
 
+void VEdit::updateConfig()
+{
+    m_config.init(QFontMetrics(font()));
+
+    if (m_config.m_tabStopWidth > 0) {
+        setTabStopWidth(m_config.m_tabStopWidth);
+    }
+
+    emit configUpdated();
+}
+
 void VEdit::beginEdit()
 {
     updateFontAndPalette();
 
+    updateConfig();
+
     setReadOnly(false);
     setModified(false);
 }
@@ -404,7 +440,7 @@ void VEdit::highlightCurrentLine()
     if (vconfig.getHighlightCursorLine() && !isReadOnly()) {
         // Need to highlight current line.
         QTextEdit::ExtraSelection select;
-        select.format.setBackground(m_cursorLineColor);
+        select.format.setBackground(m_config.m_cursorLineBg);
         select.format.setProperty(QTextFormat::FullWidthSelection, true);
         select.cursor = textCursor();
         select.cursor.clearSelection();
@@ -651,3 +687,7 @@ void VEdit::handleCursorPositionChanged()
     lastCursor = cursor;
 }
 
+VEditConfig &VEdit::getConfig()
+{
+    return m_config;
+}

+ 35 - 4
src/vedit.h

@@ -7,6 +7,7 @@
 #include <QVector>
 #include <QList>
 #include <QColor>
+#include <QFontMetrics>
 #include "vconstants.h"
 #include "vtoc.h"
 #include "vfile.h"
@@ -23,6 +24,27 @@ enum class SelectionId {
     MaxSelection
 };
 
+class VEditConfig {
+public:
+    VEditConfig() : m_tabStopWidth(0), m_tabSpaces("\t"),
+                    m_enableVimMode(false) {}
+
+    void init(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;
+};
+
 class VEdit : public QTextEdit
 {
     Q_OBJECT
@@ -51,11 +73,19 @@ public:
     void clearSearchedWordHighlight();
     VFile *getFile() const;
 
+    VEditConfig &getConfig();
+
 signals:
     void saveAndRead();
     void discardAndRead();
     void editNote();
 
+    // Emit when m_config has been updated.
+    void configUpdated();
+
+public slots:
+    virtual void highlightCurrentLine();
+
 private slots:
     void labelTimerTimeout();
     void highlightSelectedWord();
@@ -65,17 +95,18 @@ private slots:
     void highlightTrailingSpace();
     void handleCursorPositionChanged();
 
-protected slots:
-    virtual void highlightCurrentLine();
-
 protected:
     QPointer<VFile> m_file;
     VEditOperations *m_editOps;
-    QColor m_cursorLineColor;
+
+    VEditConfig m_config;
 
     virtual void updateFontAndPalette();
     virtual void contextMenuEvent(QContextMenuEvent *p_event) Q_DECL_OVERRIDE;
 
+    // Update m_config according to VConfigManager.
+    void updateConfig();
+
 private:
     QLabel *m_wrapLabel;
     QTimer *m_labelTimer;

+ 49 - 11
src/veditoperations.cpp

@@ -4,14 +4,20 @@
 #include "vedit.h"
 #include "veditoperations.h"
 #include "vconfigmanager.h"
+#include "utils/vutils.h"
 
 extern VConfigManager vconfig;
 
 VEditOperations::VEditOperations(VEdit *p_editor, VFile *p_file)
-    : QObject(p_editor), m_editor(p_editor), m_file(p_file), m_expandTab(false),
-      m_keyState(KeyState::Normal), m_pendingTime(2)
+    : QObject(p_editor), m_editor(p_editor), m_file(p_file),
+      m_editConfig(&p_editor->getConfig())
 {
-    updateTabSettings();
+    m_vim = new VVim(m_editor);
+
+    connect(m_editor, &VEdit::configUpdated,
+            this, &VEditOperations::handleEditConfigUpdated);
+    connect(m_vim, &VVim::modeChanged,
+            this, &VEditOperations::handleVimModeChanged);
 }
 
 void VEditOperations::insertTextAtCurPos(const QString &p_text)
@@ -25,14 +31,46 @@ VEditOperations::~VEditOperations()
 {
 }
 
-void VEditOperations::updateTabSettings()
+void VEditOperations::updateCursorLineBg()
 {
-    if (vconfig.getTabStopWidth() > 0) {
-        QFontMetrics metrics(vconfig.getMdEditFont());
-        m_editor->setTabStopWidth(vconfig.getTabStopWidth() * metrics.width(' '));
-    }
-    m_expandTab = vconfig.getIsExpandTab();
-    if (m_expandTab && (vconfig.getTabStopWidth() > 0)) {
-        m_tabSpaces = QString(vconfig.getTabStopWidth(), ' ');
+    if (m_editConfig->m_enableVimMode) {
+        switch (m_vim->getMode()) {
+        case VimMode::Normal:
+            m_editConfig->m_cursorLineBg = QColor(vconfig.getEditorVimNormalBg());
+            break;
+
+        case VimMode::Insert:
+            m_editConfig->m_cursorLineBg = QColor(vconfig.getEditorVimInsertBg());
+            break;
+
+        case VimMode::Visual:
+        case VimMode::VisualLine:
+            m_editConfig->m_cursorLineBg = QColor(vconfig.getEditorVimVisualBg());
+            break;
+
+        case VimMode::Replace:
+            m_editConfig->m_cursorLineBg = QColor(vconfig.getEditorVimReplaceBg());
+            break;
+
+        default:
+            V_ASSERT(false);
+            break;
+        }
+    } else {
+        m_editConfig->m_cursorLineBg = QColor(vconfig.getEditorCurrentLineBg());
     }
+
+    m_editor->highlightCurrentLine();
+}
+
+void VEditOperations::handleEditConfigUpdated()
+{
+    updateCursorLineBg();
+}
+
+void VEditOperations::handleVimModeChanged(VimMode p_mode)
+{
+    Q_UNUSED(p_mode);
+
+    updateCursorLineBg();
 }

+ 15 - 11
src/veditoperations.h

@@ -6,13 +6,13 @@
 #include <QObject>
 #include <QList>
 #include "vfile.h"
+#include "utils/vvim.h"
 
 class VEdit;
+class VEditConfig;
 class QMimeData;
 class QKeyEvent;
 
-enum class KeyState { Normal = 0, Vim, VimVisual};
-
 class VEditOperations: public QObject
 {
     Q_OBJECT
@@ -22,25 +22,29 @@ public:
     virtual bool insertImageFromMimeData(const QMimeData *source) = 0;
     virtual bool insertImage() = 0;
     virtual bool insertImageFromURL(const QUrl &p_imageUrl) = 0;
+
     // Return true if @p_event has been handled and no need to be further
     // processed.
     virtual bool handleKeyPressEvent(QKeyEvent *p_event) = 0;
-    void updateTabSettings();
 
-signals:
-    void keyStateChanged(KeyState p_state);
+protected slots:
+    // Handle the update of VEditConfig of the editor.
+    virtual void handleEditConfigUpdated();
+
+    // Vim mode changed.
+    void handleVimModeChanged(VimMode p_mode);
+
+private:
+    // Update m_editConfig->m_cursorLineBg.
+    void updateCursorLineBg();
 
 protected:
     void insertTextAtCurPos(const QString &p_text);
 
     VEdit *m_editor;
     QPointer<VFile> m_file;
-    bool m_expandTab;
-    QString m_tabSpaces;
-    KeyState m_keyState;
-    // Seconds for pending mode.
-    int m_pendingTime;
-    QList<QString> m_pendingKey;
+    VEditConfig *m_editConfig;
+    VVim *m_vim;
 };
 
 #endif // VEDITOPERATIONS_H

+ 15 - 0
src/vmainwindow.cpp

@@ -605,6 +605,13 @@ void VMainWindow::initEditMenu()
     connect(autoListAct, &QAction::triggered,
             this, &VMainWindow::changeAutoList);
 
+    // Vim Mode.
+    QAction *vimAct = new QAction(tr("Vim Mode"), this);
+    vimAct->setToolTip(tr("Enable Vim mode for editing (re-enter edit mode to make it work)"));
+    vimAct->setCheckable(true);
+    connect(vimAct, &QAction::triggered,
+            this, &VMainWindow::changeVimMode);
+
     // Highlight current cursor line.
     QAction *cursorLineAct = new QAction(tr("Highlight Cursor Line"), this);
     cursorLineAct->setToolTip(tr("Highlight current cursor line"));
@@ -687,6 +694,9 @@ void VMainWindow::initEditMenu()
     }
     Q_ASSERT(!(autoListAct->isChecked() && !m_autoIndentAct->isChecked()));
 
+    editMenu->addAction(vimAct);
+    vimAct->setChecked(vconfig.getEnableVimMode());
+
     editMenu->addSeparator();
 
     initEditorStyleMenu(editMenu);
@@ -1381,6 +1391,11 @@ void VMainWindow::changeAutoList(bool p_checked)
     }
 }
 
+void VMainWindow::changeVimMode(bool p_checked)
+{
+    vconfig.setEnableVimMode(p_checked);
+}
+
 void VMainWindow::enableCodeBlockHighlight(bool p_checked)
 {
     vconfig.setEnableCodeBlockHighlight(p_checked);

+ 1 - 0
src/vmainwindow.h

@@ -76,6 +76,7 @@ private slots:
     void handleCaptainModeChanged(bool p_enabled);
     void changeAutoIndent(bool p_checked);
     void changeAutoList(bool p_checked);
+    void changeVimMode(bool p_checked);
     void enableCodeBlockHighlight(bool p_checked);
     void enableImagePreview(bool p_checked);
     void enableImagePreviewConstraint(bool p_checked);

+ 4 - 16
src/vmdedit.cpp

@@ -45,8 +45,6 @@ VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type,
     m_imagePreviewer = new VImagePreviewer(this, 500);
 
     m_editOps = new VMdEditOperations(this, m_file);
-    connect(m_editOps, &VEditOperations::keyStateChanged,
-            this, &VMdEdit::handleEditStateChanged);
 
     connect(this, &VMdEdit::cursorPositionChanged,
             this, &VMdEdit::updateCurHeader);
@@ -56,22 +54,23 @@ VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type,
     connect(QApplication::clipboard(), &QClipboard::changed,
             this, &VMdEdit::handleClipboardChanged);
 
-    m_editOps->updateTabSettings();
     updateFontAndPalette();
+
+    updateConfig();
 }
 
 void VMdEdit::updateFontAndPalette()
 {
     setFont(vconfig.getMdEditFont());
     setPalette(vconfig.getMdEditPalette());
-    m_cursorLineColor = vconfig.getEditorCurrentLineBackground();
 }
 
 void VMdEdit::beginEdit()
 {
-    m_editOps->updateTabSettings();
     updateFontAndPalette();
 
+    updateConfig();
+
     Q_ASSERT(m_file->getContent() == toPlainTextWithoutImg());
 
     initInitImages();
@@ -366,17 +365,6 @@ int VMdEdit::removeObjectReplacementLine(QString &p_text, int p_index) const
     return prevLineIdx - 1;
 }
 
-void VMdEdit::handleEditStateChanged(KeyState p_state)
-{
-    qDebug() << "edit state" << (int)p_state;
-    if (p_state == KeyState::Normal) {
-        m_cursorLineColor = vconfig.getEditorCurrentLineBackground();
-    } else {
-        m_cursorLineColor = vconfig.getEditorCurrentLineVimBackground();
-    }
-    highlightCurrentLine();
-}
-
 void VMdEdit::handleSelectionChanged()
 {
     if (!vconfig.getEnablePreviewImages()) {

+ 0 - 1
src/vmdedit.h

@@ -53,7 +53,6 @@ private slots:
     // When there is no header in current cursor, will signal an invalid header.
     void updateCurHeader();
 
-    void handleEditStateChanged(KeyState p_state);
     void handleSelectionChanged();
     void handleClipboardChanged(QClipboard::Mode p_mode);
 

+ 120 - 577
src/vmdeditoperations.cpp

@@ -21,6 +21,7 @@
 #include "vfile.h"
 #include "vmdedit.h"
 #include "vconfigmanager.h"
+#include "utils/vvim.h"
 
 extern VConfigManager vconfig;
 
@@ -29,10 +30,6 @@ const QString VMdEditOperations::c_defaultImageTitle = "image";
 VMdEditOperations::VMdEditOperations(VEdit *p_editor, VFile *p_file)
     : VEditOperations(p_editor, p_file), m_autoIndentPos(-1)
 {
-    m_pendingTimer = new QTimer(this);
-    m_pendingTimer->setSingleShot(true);
-    m_pendingTimer->setInterval(m_pendingTime * 1000);  // milliseconds
-    connect(m_pendingTimer, &QTimer::timeout, this, &VMdEditOperations::pendingTimerTimeout);
 }
 
 bool VMdEditOperations::insertImageFromMimeData(const QMimeData *source)
@@ -193,162 +190,137 @@ bool VMdEditOperations::insertImage()
     return true;
 }
 
-// Will modify m_pendingKey.
-bool VMdEditOperations::shouldTriggerVimMode(QKeyEvent *p_event)
+bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event)
 {
-    int modifiers = p_event->modifiers();
-    int key = p_event->key();
-    if (key == Qt::Key_Escape ||
-        (key == Qt::Key_BracketLeft && modifiers == Qt::ControlModifier)) {
-        return false;
-    } else if (m_keyState == KeyState::Vim || m_keyState == KeyState::VimVisual) {
+    if (m_editConfig->m_enableVimMode && m_vim->handleKeyPressEvent(p_event)) {
+        m_autoIndentPos = -1;
         return true;
     }
-    return false;
-}
 
-bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event)
-{
     bool ret = false;
     int key = p_event->key();
     int modifiers = p_event->modifiers();
 
-    if (shouldTriggerVimMode(p_event)) {
-        if (handleKeyPressVim(p_event)) {
-            ret = true;
-            goto exit;
-        }
-    } else {
-        switch (key) {
-        case Qt::Key_1:
-        case Qt::Key_2:
-        case Qt::Key_3:
-        case Qt::Key_4:
-        case Qt::Key_5:
-        case Qt::Key_6:
-        {
-            if (modifiers == Qt::ControlModifier) {
-                // Ctrl + <N>: insert title at level <N>.
-                if (insertTitle(key - Qt::Key_0)) {
-                    p_event->accept();
-                    ret = true;
-                    goto exit;
-                }
-            }
-            break;
-        }
-
-        case Qt::Key_Tab:
-        {
-            if (handleKeyTab(p_event)) {
+    switch (key) {
+    case Qt::Key_1:
+    case Qt::Key_2:
+    case Qt::Key_3:
+    case Qt::Key_4:
+    case Qt::Key_5:
+    case Qt::Key_6:
+    {
+        if (modifiers == Qt::ControlModifier) {
+            // Ctrl + <N>: insert title at level <N>.
+            if (insertTitle(key - Qt::Key_0)) {
+                p_event->accept();
                 ret = true;
                 goto exit;
             }
-            break;
         }
+        break;
+    }
 
-        case Qt::Key_Backtab:
-        {
-            if (handleKeyBackTab(p_event)) {
-                ret = true;
-                goto exit;
-            }
-            break;
+    case Qt::Key_Tab:
+    {
+        if (handleKeyTab(p_event)) {
+            ret = true;
+            goto exit;
         }
+        break;
+    }
 
-        case Qt::Key_B:
-        {
-            if (handleKeyB(p_event)) {
-                ret = true;
-                goto exit;
-            }
-            break;
+    case Qt::Key_Backtab:
+    {
+        if (handleKeyBackTab(p_event)) {
+            ret = true;
+            goto exit;
         }
+        break;
+    }
 
-        case Qt::Key_D:
-        {
-            if (handleKeyD(p_event)) {
-                ret = true;
-                goto exit;
-            }
-            break;
+    case Qt::Key_B:
+    {
+        if (handleKeyB(p_event)) {
+            ret = true;
+            goto exit;
         }
+        break;
+    }
 
-        case Qt::Key_H:
-        {
-            if (handleKeyH(p_event)) {
-                ret = true;
-                goto exit;
-            }
-            break;
+    case Qt::Key_H:
+    {
+        if (handleKeyH(p_event)) {
+            ret = true;
+            goto exit;
         }
+        break;
+    }
 
-        case Qt::Key_I:
-        {
-            if (handleKeyI(p_event)) {
-                ret = true;
-                goto exit;
-            }
-            break;
+    case Qt::Key_I:
+    {
+        if (handleKeyI(p_event)) {
+            ret = true;
+            goto exit;
         }
+        break;
+    }
 
-        case Qt::Key_O:
-        {
-            if (handleKeyO(p_event)) {
-                ret = true;
-                goto exit;
-            }
-            break;
+    case Qt::Key_O:
+    {
+        if (handleKeyO(p_event)) {
+            ret = true;
+            goto exit;
         }
+        break;
+    }
 
-        case Qt::Key_U:
-        {
-            if (handleKeyU(p_event)) {
-                ret = true;
-                goto exit;
-            }
-            break;
+    case Qt::Key_U:
+    {
+        if (handleKeyU(p_event)) {
+            ret = true;
+            goto exit;
         }
+        break;
+    }
 
-        case Qt::Key_W:
-        {
-            if (handleKeyW(p_event)) {
-                ret = true;
-                goto exit;
-            }
-            break;
+    case Qt::Key_W:
+    {
+        if (handleKeyW(p_event)) {
+            ret = true;
+            goto exit;
         }
+        break;
+    }
 
-        case Qt::Key_BracketLeft:
-        {
-            if (handleKeyBracketLeft(p_event)) {
-                ret = true;
-                goto exit;
-            }
-            break;
+    case Qt::Key_BracketLeft:
+    {
+        if (handleKeyBracketLeft(p_event)) {
+            ret = true;
+            goto exit;
         }
+        break;
+    }
 
-        case Qt::Key_Escape:
-        {
-            if (handleKeyEsc(p_event)) {
-                ret = true;
-                goto exit;
-            }
-            break;
+    case Qt::Key_Escape:
+    {
+        if (handleKeyEsc(p_event)) {
+            ret = true;
+            goto exit;
         }
+        break;
+    }
 
-        case Qt::Key_Return:
-        {
-            if (handleKeyReturn(p_event)) {
-                ret = true;
-                goto exit;
-            }
-            break;
+    case Qt::Key_Return:
+    {
+        if (handleKeyReturn(p_event)) {
+            ret = true;
+            goto exit;
         }
+        break;
+    }
 
-        default:
-            break;
-        }
+    default:
+        break;
     }
 
 exit:
@@ -357,44 +329,32 @@ exit:
         key != Qt::Key_Shift) {
         m_autoIndentPos = -1;
     }
+
     return ret;
 }
 
 // Let Ctrl+[ behave exactly like ESC.
 bool VMdEditOperations::handleKeyBracketLeft(QKeyEvent *p_event)
 {
-    // 1. If it is not in Normal state, just go back to Normal state;
-    // 2. If it is already Normal state, try to clear selection;
-    // 3. Otherwise, ignore this event and let parent handles it.
-    bool accept = false;
+    // 1. If there is any selection, clear it.
+    // 2. Otherwise, ignore this event and let parent handles it.
     if (p_event->modifiers() == Qt::ControlModifier) {
-        if (m_keyState != KeyState::Normal) {
-            m_pendingTimer->stop();
-            setKeyState(KeyState::Normal);
-            m_pendingKey.clear();
-            accept = true;
-        } else {
-            QTextCursor cursor = m_editor->textCursor();
-            if (cursor.hasSelection()) {
-                cursor.clearSelection();
-                m_editor->setTextCursor(cursor);
-                accept = true;
-            }
+        QTextCursor cursor = m_editor->textCursor();
+        if (cursor.hasSelection()) {
+            cursor.clearSelection();
+            m_editor->setTextCursor(cursor);
+            p_event->accept();
+            return true;
         }
     }
-    if (accept) {
-        p_event->accept();
-    }
-    return accept;
+
+    return false;
 }
 
 bool VMdEditOperations::handleKeyTab(QKeyEvent *p_event)
 {
     QTextDocument *doc = m_editor->document();
-    QString text("\t");
-    if (m_expandTab) {
-        text = m_tabSpaces;
-    }
+    QString text(m_editConfig->m_tabSpaces);
 
     if (p_event->modifiers() == Qt::NoModifier) {
         QTextCursor cursor = m_editor->textCursor();
@@ -483,8 +443,8 @@ bool VMdEditOperations::handleKeyBackTab(QKeyEvent *p_event)
             continue;
         } else {
             // Spaces.
-            if (m_expandTab) {
-                int width = m_tabSpaces.size();
+            if (m_editConfig->m_expandTab) {
+                int width = m_editConfig->m_tabSpaces.size();
                 for (int i = 0; i < width; ++i) {
                     if (text[i] == ' ') {
                         blockCursor.deleteChar();
@@ -555,20 +515,6 @@ bool VMdEditOperations::handleKeyB(QKeyEvent *p_event)
     return false;
 }
 
-bool VMdEditOperations::handleKeyD(QKeyEvent *p_event)
-{
-    if (p_event->modifiers() == Qt::ControlModifier) {
-        // Ctrl+D, enter Vim-pending mode.
-        // Will accept the key stroke in m_pendingTime as Vim normal command.
-        setKeyState(KeyState::Vim);
-        m_pendingTimer->stop();
-        m_pendingTimer->start();
-        p_event->accept();
-        return true;
-    }
-    return false;
-}
-
 bool VMdEditOperations::handleKeyH(QKeyEvent *p_event)
 {
     if (p_event->modifiers() == Qt::ControlModifier) {
@@ -713,27 +659,17 @@ bool VMdEditOperations::handleKeyW(QKeyEvent *p_event)
 
 bool VMdEditOperations::handleKeyEsc(QKeyEvent *p_event)
 {
-    // 1. If it is not in Normal state, just go back to Normal state;
-    // 2. If it is already Normal state, try to clear selection;
-    // 3. Otherwise, ignore this event and let parent handles it.
-    bool accept = false;
-    if (m_keyState != KeyState::Normal) {
-        m_pendingTimer->stop();
-        setKeyState(KeyState::Normal);
-        m_pendingKey.clear();
-        accept = true;
-    } else {
-        QTextCursor cursor = m_editor->textCursor();
-        if (cursor.hasSelection()) {
-            cursor.clearSelection();
-            m_editor->setTextCursor(cursor);
-            accept = true;
-        }
-    }
-    if (accept) {
+    // 1. If there is any selection, clear it.
+    // 2. Otherwise, ignore this event and let parent handles it.
+    QTextCursor cursor = m_editor->textCursor();
+    if (cursor.hasSelection()) {
+        cursor.clearSelection();
+        m_editor->setTextCursor(cursor);
         p_event->accept();
+        return true;
     }
-    return accept;
+
+    return false;
 }
 
 bool VMdEditOperations::handleKeyReturn(QKeyEvent *p_event)
@@ -935,399 +871,6 @@ void VMdEditOperations::deleteIndentAndListMark()
     m_editor->setTextCursor(cursor);
 }
 
-bool VMdEditOperations::handleKeyPressVim(QKeyEvent *p_event)
-{
-    int modifiers = p_event->modifiers();
-    bool visualMode = m_keyState == KeyState::VimVisual;
-    QTextCursor::MoveMode mode = visualMode ? QTextCursor::KeepAnchor
-                                              : QTextCursor::MoveAnchor;
-
-    switch (p_event->key()) {
-    // Ctrl and Shift may be sent out first.
-    case Qt::Key_Control:
-        // Fall through.
-    case Qt::Key_Shift:
-    {
-        goto pending;
-        break;
-    }
-
-    case Qt::Key_H:
-    case Qt::Key_J:
-    case Qt::Key_K:
-    case Qt::Key_L:
-    {
-        if (modifiers == Qt::NoModifier) {
-            QTextCursor::MoveOperation op = QTextCursor::Left;
-            switch (p_event->key()) {
-            case Qt::Key_H:
-                op = QTextCursor::Left;
-                break;
-            case Qt::Key_J:
-                op = QTextCursor::Down;
-                break;
-            case Qt::Key_K:
-                op = QTextCursor::Up;
-                break;
-            case Qt::Key_L:
-                op = QTextCursor::Right;
-            }
-            // Move cursor <repeat> characters left/Down/Up/Right.
-            int repeat = keySeqToNumber(m_pendingKey);
-            m_pendingKey.clear();
-            QTextCursor cursor = m_editor->textCursor();
-            cursor.movePosition(op, mode, repeat == 0 ? 1 : repeat);
-            m_editor->setTextCursor(cursor);
-            goto pending;
-        }
-        break;
-    }
-
-    case Qt::Key_1:
-    case Qt::Key_2:
-    case Qt::Key_3:
-    case Qt::Key_4:
-    case Qt::Key_5:
-    case Qt::Key_6:
-    case Qt::Key_7:
-    case Qt::Key_8:
-    case Qt::Key_9:
-    {
-        if (modifiers == Qt::NoModifier) {
-            if (!suffixNumAllowed(m_pendingKey)) {
-                break;
-            }
-            int num = p_event->key() - Qt::Key_0;
-            m_pendingKey.append(QString::number(num));
-            goto pending;
-        }
-        break;
-    }
-
-    case Qt::Key_X:
-    {
-        // Delete characters.
-        if (modifiers == Qt::NoModifier) {
-            int repeat = keySeqToNumber(m_pendingKey);
-            m_pendingKey.clear();
-            QTextCursor cursor = m_editor->textCursor();
-            if (repeat == 0) {
-                repeat = 1;
-            }
-            cursor.beginEditBlock();
-            if (cursor.hasSelection()) {
-                QClipboard *clipboard = QApplication::clipboard();
-                clipboard->setText(cursor.selectedText());
-                cursor.removeSelectedText();
-            } else {
-                for (int i = 0; i < repeat; ++i) {
-                    cursor.deleteChar();
-                }
-            }
-            cursor.endEditBlock();
-            m_editor->setTextCursor(cursor);
-            goto pending;
-        }
-        break;
-    }
-
-    case Qt::Key_W:
-    {
-        if (modifiers == Qt::NoModifier) {
-            // Move to the start of the next word.
-            // Slightly different from the Vim behavior.
-            int repeat = keySeqToNumber(m_pendingKey);
-            m_pendingKey.clear();
-            QTextCursor cursor = m_editor->textCursor();
-            if (repeat == 0) {
-                repeat = 1;
-            }
-            cursor.movePosition(QTextCursor::NextWord, mode, repeat);
-            m_editor->setTextCursor(cursor);
-            goto pending;
-        }
-        break;
-    }
-
-    case Qt::Key_E:
-    {
-        if (modifiers == Qt::NoModifier) {
-            // Move to the end of the next word.
-            // Slightly different from the Vim behavior.
-            int repeat = keySeqToNumber(m_pendingKey);
-            m_pendingKey.clear();
-            QTextCursor cursor = m_editor->textCursor();
-            if (repeat == 0) {
-                repeat = 1;
-            }
-            cursor.beginEditBlock();
-            int pos = cursor.position();
-            // First move to the end of current word.
-            cursor.movePosition(QTextCursor::EndOfWord, mode);
-            if (cursor.position() != pos) {
-                // We did move.
-                repeat--;
-            }
-            if (repeat) {
-                cursor.movePosition(QTextCursor::NextWord, mode, repeat);
-                cursor.movePosition(QTextCursor::EndOfWord, mode);
-            }
-            cursor.endEditBlock();
-            m_editor->setTextCursor(cursor);
-            goto pending;
-        }
-        break;
-    }
-
-    case Qt::Key_B:
-    {
-        if (modifiers == Qt::NoModifier) {
-            // Move to the start of the previous word.
-            // Slightly different from the Vim behavior.
-            int repeat = keySeqToNumber(m_pendingKey);
-            m_pendingKey.clear();
-            QTextCursor cursor = m_editor->textCursor();
-            if (repeat == 0) {
-                repeat = 1;
-            }
-            cursor.beginEditBlock();
-            int pos = cursor.position();
-            // First move to the start of current word.
-            cursor.movePosition(QTextCursor::StartOfWord, mode);
-            if (cursor.position() != pos) {
-                // We did move.
-                repeat--;
-            }
-            if (repeat) {
-                cursor.movePosition(QTextCursor::PreviousWord, mode, repeat);
-            }
-            cursor.endEditBlock();
-            m_editor->setTextCursor(cursor);
-            goto pending;
-        }
-        break;
-    }
-
-    case Qt::Key_0:
-    {
-        if (modifiers == Qt::NoModifier) {
-            if (keySeqToNumber(m_pendingKey) == 0) {
-                QTextCursor cursor = m_editor->textCursor();
-                cursor.movePosition(QTextCursor::StartOfLine, mode);
-                m_editor->setTextCursor(cursor);
-            } else {
-                m_pendingKey.append("0");
-            }
-            goto pending;
-        }
-        break;
-    }
-
-    case Qt::Key_Dollar:
-    {
-        if (modifiers == Qt::ShiftModifier) {
-            if (m_pendingKey.isEmpty()) {
-                // Go to end of line.
-                QTextCursor cursor = m_editor->textCursor();
-                cursor.movePosition(QTextCursor::EndOfLine, mode);
-                m_editor->setTextCursor(cursor);
-                goto pending;
-            }
-        }
-        break;
-    }
-
-    case Qt::Key_AsciiCircum:
-    {
-        if (modifiers == Qt::ShiftModifier) {
-            if (m_pendingKey.isEmpty()) {
-                // Go to first non-space character of current line.
-                QTextCursor cursor = m_editor->textCursor();
-                QTextBlock block = cursor.block();
-                QString text = block.text();
-                cursor.beginEditBlock();
-                if (text.trimmed().isEmpty()) {
-                    cursor.movePosition(QTextCursor::StartOfLine, mode);
-                } else {
-                    cursor.movePosition(QTextCursor::StartOfLine, mode);
-                    int pos = cursor.positionInBlock();
-                    while (pos < text.size() && text[pos].isSpace()) {
-                        cursor.movePosition(QTextCursor::NextWord, mode);
-                        pos = cursor.positionInBlock();
-                    }
-                }
-                cursor.endEditBlock();
-                m_editor->setTextCursor(cursor);
-                goto pending;
-            }
-        }
-        break;
-    }
-
-    case Qt::Key_G:
-    {
-        if (modifiers == Qt::NoModifier) {
-            // g, pending or go to first line.
-            if (m_pendingKey.isEmpty()) {
-                m_pendingKey.append("g");
-                goto pending;
-            } else if (m_pendingKey.size() == 1 && m_pendingKey.at(0) == "g") {
-                m_pendingKey.clear();
-                QTextCursor cursor = m_editor->textCursor();
-                cursor.movePosition(QTextCursor::Start, mode);
-                m_editor->setTextCursor(cursor);
-                goto pending;
-            }
-        } else if (modifiers == Qt::ShiftModifier) {
-            // G, go to a certain line or the end of document.
-            int lineNum = keySeqToNumber(m_pendingKey);
-            m_pendingKey.clear();
-            QTextCursor cursor = m_editor->textCursor();
-            if (lineNum == 0) {
-                cursor.movePosition(QTextCursor::End, mode);
-            } else {
-                QTextDocument *doc = m_editor->document();
-                QTextBlock block = doc->findBlockByNumber(lineNum - 1);
-                if (block.isValid()) {
-                    cursor.setPosition(block.position(), mode);
-                } else {
-                    // Go beyond the document.
-                    cursor.movePosition(QTextCursor::End, mode);
-                }
-            }
-            m_editor->setTextCursor(cursor);
-            goto pending;
-        }
-        break;
-    }
-
-    case Qt::Key_V:
-    {
-        if (modifiers == Qt::NoModifier) {
-            // V to enter visual mode.
-            if (m_pendingKey.isEmpty() && m_keyState != KeyState::VimVisual) {
-                setKeyState(KeyState::VimVisual);
-                goto pending;
-            }
-        }
-        break;
-    }
-
-    case Qt::Key_Y:
-    {
-        if (modifiers == Qt::NoModifier) {
-            if (m_pendingKey.isEmpty()) {
-                QTextCursor cursor = m_editor->textCursor();
-                if (cursor.hasSelection()) {
-                    QString text = cursor.selectedText();
-                    QClipboard *clipboard = QApplication::clipboard();
-                    clipboard->setText(text);
-                }
-                goto pending;
-            }
-        }
-        break;
-    }
-
-    case Qt::Key_D:
-    {
-        if (modifiers == Qt::NoModifier) {
-            // d, pending or delete current line.
-            QTextCursor cursor = m_editor->textCursor();
-            if (m_pendingKey.isEmpty()) {
-                if (cursor.hasSelection()) {
-                    cursor.deleteChar();
-                    m_editor->setTextCursor(cursor);
-                } else {
-                    m_pendingKey.append("d");
-                }
-                goto pending;
-            } else if (m_pendingKey.size() == 1 && m_pendingKey.at(0) == "d") {
-                m_pendingKey.clear();
-                cursor.select(QTextCursor::BlockUnderCursor);
-                cursor.removeSelectedText();
-                m_editor->setTextCursor(cursor);
-                goto pending;
-            }
-        }
-        break;
-    }
-
-    default:
-        // Unknown key. End Vim mode.
-        break;
-    }
-
-    m_pendingTimer->stop();
-    if (m_keyState == KeyState::VimVisual) {
-        // Clear the visual selection.
-        QTextCursor cursor = m_editor->textCursor();
-        cursor.clearSelection();
-        m_editor->setTextCursor(cursor);
-    }
-    setKeyState(KeyState::Normal);
-    m_pendingKey.clear();
-    p_event->accept();
-    return true;
-
-pending:
-    // When pending in Ctrl+Alt, we just want to clear m_pendingKey.
-    if (m_pendingTimer->isActive()) {
-        m_pendingTimer->stop();
-        m_pendingTimer->start();
-    }
-    p_event->accept();
-    return true;
-}
-
-int VMdEditOperations::keySeqToNumber(const QList<QString> &p_seq)
-{
-    int num = 0;
-    for (int i = 0; i < p_seq.size(); ++i) {
-        QString tmp = p_seq.at(i);
-        bool ok;
-        int tmpInt = tmp.toInt(&ok);
-        if (!ok) {
-            return 0;
-        }
-        num = num * 10 + tmpInt;
-    }
-    return num;
-}
-
-void VMdEditOperations::pendingTimerTimeout()
-{
-    qDebug() << "key pending timer timeout";
-    if (m_keyState == KeyState::VimVisual) {
-        m_pendingTimer->start();
-        return;
-    }
-    setKeyState(KeyState::Normal);
-    m_pendingKey.clear();
-}
-
-bool VMdEditOperations::suffixNumAllowed(const QList<QString> &p_seq)
-{
-    if (!p_seq.isEmpty()) {
-        QString firstEle = p_seq.at(0);
-        if (firstEle[0].isDigit()) {
-            return true;
-        } else {
-            return false;
-        }
-    }
-    return true;
-}
-
-void VMdEditOperations::setKeyState(KeyState p_state)
-{
-    if (m_keyState == p_state) {
-        return;
-    }
-    m_keyState = p_state;
-    emit keyStateChanged(m_keyState);
-}
-
 bool VMdEditOperations::insertTitle(int p_level)
 {
     Q_ASSERT(p_level > 0 && p_level < 7);

+ 0 - 11
src/vmdeditoperations.h

@@ -21,9 +21,6 @@ public:
     bool handleKeyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE;
     bool insertImageFromURL(const QUrl &p_imageUrl) Q_DECL_OVERRIDE;
 
-private slots:
-    void pendingTimerTimeout();
-
 private:
     void insertImageFromPath(const QString &title, const QString &path, const QString &oriImagePath);
 
@@ -32,13 +29,10 @@ private:
     // @image: the image to be inserted;
     void insertImageFromQImage(const QString &title, const QString &path, const QImage &image);
 
-    void setKeyState(KeyState p_state);
-
     // Key press handlers.
     bool handleKeyTab(QKeyEvent *p_event);
     bool handleKeyBackTab(QKeyEvent *p_event);
     bool handleKeyB(QKeyEvent *p_event);
-    bool handleKeyD(QKeyEvent *p_event);
     bool handleKeyH(QKeyEvent *p_event);
     bool handleKeyI(QKeyEvent *p_event);
     bool handleKeyO(QKeyEvent *p_event);
@@ -46,11 +40,7 @@ private:
     bool handleKeyW(QKeyEvent *p_event);
     bool handleKeyEsc(QKeyEvent *p_event);
     bool handleKeyReturn(QKeyEvent *p_event);
-    bool handleKeyPressVim(QKeyEvent *p_event);
     bool handleKeyBracketLeft(QKeyEvent *p_event);
-    bool shouldTriggerVimMode(QKeyEvent *p_event);
-    int keySeqToNumber(const QList<QString> &p_seq);
-    bool suffixNumAllowed(const QList<QString> &p_seq);
     bool insertTitle(int p_level);
     bool insertNewBlockWithIndent();
     bool insertListMarkAsPreviousLine();
@@ -67,7 +57,6 @@ private:
     // Change the sequence number of a list block.
     void changeListBlockSeqNumber(QTextBlock &p_block, int p_seq);
 
-    QTimer *m_pendingTimer;
     // The cursor position after auto indent or auto list.
     // It will be -1 if last key press do not trigger the auto indent or auto list.
     int m_autoIndentPos;