Browse Source

vim-mode: support command line mode and leader key

1. We now support limited commands: :w, :q, :wq, :x, :q! ;
2. We now support fixed leader sequence: y, d, p, P ;
3. Support <num>% to goto <num>/100 percent of the document.
Le Tan 8 years ago
parent
commit
10a9447b96
5 changed files with 503 additions and 81 deletions
  1. 431 77
      src/utils/vvim.cpp
  2. 54 2
      src/utils/vvim.h
  3. 8 0
      src/vedit.h
  4. 8 2
      src/vmdeditoperations.cpp
  5. 2 0
      src/vmdtab.cpp

+ 431 - 77
src/utils/vvim.cpp

@@ -18,10 +18,74 @@ const QChar VVim::c_unnamedRegister = QChar('"');
 const QChar VVim::c_blackHoleRegister = QChar('_');
 const QChar VVim::c_selectionRegister = QChar('+');
 
+#define ADDKEY(x, y) case (x): {ch = (y); break;}
+
+// Returns NULL QChar if invalid.
+static QChar keyToChar(int p_key, int p_modifiers)
+{
+    if (p_modifiers == Qt::ControlModifier) {
+        return QChar();
+    }
+
+    if (p_key >= Qt::Key_0 && p_key <= Qt::Key_9) {
+        return QChar('0' + (p_key - Qt::Key_0));
+    } else if (p_key >= Qt::Key_A && p_key <= Qt::Key_Z) {
+        if (p_modifiers == Qt::ShiftModifier) {
+            return QChar('A' + (p_key - Qt::Key_A));
+        } else {
+            return QChar('a' + (p_key - Qt::Key_A));
+        }
+    }
+
+    QChar ch;
+    switch (p_key) {
+    ADDKEY(Qt::Key_Tab, '\t');
+    ADDKEY(Qt::Key_Space, ' ');
+    ADDKEY(Qt::Key_Exclam, '!');
+    ADDKEY(Qt::Key_QuoteDbl, '"');
+    ADDKEY(Qt::Key_NumberSign, '#');
+    ADDKEY(Qt::Key_Dollar, '$');
+    ADDKEY(Qt::Key_Percent, '%');
+    ADDKEY(Qt::Key_Ampersand, '&');
+    ADDKEY(Qt::Key_Apostrophe, '\'');
+    ADDKEY(Qt::Key_ParenLeft, '(');
+    ADDKEY(Qt::Key_ParenRight, ')');
+    ADDKEY(Qt::Key_Asterisk, '*');
+    ADDKEY(Qt::Key_Plus, '+');
+    ADDKEY(Qt::Key_Comma, ',');
+    ADDKEY(Qt::Key_Minus, '-');
+    ADDKEY(Qt::Key_Period, '.');
+    ADDKEY(Qt::Key_Slash, '/');
+    ADDKEY(Qt::Key_Colon, ':');
+    ADDKEY(Qt::Key_Semicolon, ';');
+    ADDKEY(Qt::Key_Less, '<');
+    ADDKEY(Qt::Key_Equal, '=');
+    ADDKEY(Qt::Key_Greater, '>');
+    ADDKEY(Qt::Key_Question, '?');
+    ADDKEY(Qt::Key_At, '@');
+    ADDKEY(Qt::Key_BracketLeft, '[');
+    ADDKEY(Qt::Key_Backslash, '\\');
+    ADDKEY(Qt::Key_BracketRight, ']');
+    ADDKEY(Qt::Key_AsciiCircum, '^');
+    ADDKEY(Qt::Key_Underscore, '_');
+    ADDKEY(Qt::Key_QuoteLeft, '`');
+    ADDKEY(Qt::Key_BraceLeft, '{');
+    ADDKEY(Qt::Key_Bar, '|');
+    ADDKEY(Qt::Key_BraceRight, '}');
+    ADDKEY(Qt::Key_AsciiTilde, '~');
+
+    default:
+        break;
+    }
+
+    return ch;
+}
+
 VVim::VVim(VEdit *p_editor)
     : QObject(p_editor), m_editor(p_editor),
       m_editConfig(&p_editor->getConfig()), m_mode(VimMode::Normal),
-      m_resetPositionInBlock(true), m_regName(c_unnamedRegister)
+      m_resetPositionInBlock(true), m_regName(c_unnamedRegister),
+      m_cmdMode(false), m_leaderKey(Key(Qt::Key_Space)), m_replayLeaderSequence(false)
 {
     initRegisters();
 
@@ -203,7 +267,7 @@ static void moveCursorAcrossSpaces(QTextCursor &p_cursor,
 
 // Expand the selection of @p_cursor to contain additional spaces at the two ends
 // within a block.
-void expandSelectionAcrossSpacesWithinBlock(QTextCursor &p_cursor)
+static void expandSelectionAcrossSpacesWithinBlock(QTextCursor &p_cursor)
 {
     QTextBlock block = p_cursor.block();
     QString text = block.text();
@@ -261,11 +325,34 @@ static void insertChangeBlockAfterDeletion(QTextCursor &p_cursor, int p_deletion
     }
 }
 
+// Given the percentage of the text, return the corresponding block number.
+// Notice that the block number is based on 0.
+// Returns -1 if it is not valid.
+static int percentageToBlockNumber(const QTextDocument *p_doc, int p_percent)
+{
+    if (p_percent > 100 || p_percent <= 0) {
+        return -1;
+    }
+
+    int nrBlock = p_doc->blockCount();
+    int num = nrBlock * (p_percent * 1.0 / 100) - 1;
+
+    return num >= 0 ? num : 0;
+}
+
 bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
+{
+    bool ret = handleKeyPressEvent(p_event->key(), p_event->modifiers());
+    if (ret) {
+        p_event->accept();
+    }
+
+    return ret;
+}
+
+bool VVim::handleKeyPressEvent(int key, int modifiers)
 {
     bool ret = false;
-    int modifiers = p_event->modifiers();
-    int key = p_event->key();
     bool resetPositionInBlock = true;
     Key keyInfo(key, modifiers);
     bool unindent = false;
@@ -290,15 +377,35 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
         goto accept;
     }
 
+    if (m_replayLeaderSequence) {
+        qDebug() << "replaying sequence" << keyToChar(key, modifiers);
+    }
+
+    if (expectingCommandLineInput()) {
+        // All input will be treated as command line input.
+        // [Enter] to execute the command and exit command line mode.
+        if (processCommandLine(keyInfo)) {
+            goto clear_accept;
+        } else {
+            goto accept;
+        }
+    }
+
     m_pendingKeys.append(keyInfo);
 
+    if (expectingLeaderSequence()) {
+        if (processLeaderSequence(keyInfo)) {
+            goto accept;
+        } else {
+            goto clear_accept;
+        }
+    }
+
     if (expectingRegisterName()) {
         // Expecting a register name.
         QChar reg = keyToRegisterName(keyInfo);
         if (!reg.isNull()) {
-            // We should keep m_pendingKeys.
             m_keys.clear();
-            m_tokens.clear();
             setRegister(reg);
             if (m_registers[reg].isNamedRegister()) {
                 m_registers[reg].m_append = (modifiers == Qt::ShiftModifier);
@@ -340,12 +447,30 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
         goto clear_accept;
     }
 
+    // Check leader key here. If leader key conflicts with other keys, it will
+    // overwrite it.
+    // Leader sequence is just like an action.
+    if (keyInfo == m_leaderKey
+        && !hasActionToken()
+        && !hasNonDigitPendingKeys()
+        && !m_replayLeaderSequence) {
+        tryGetRepeatToken(m_keys, m_tokens);
+
+        Q_ASSERT(m_keys.isEmpty());
+
+        m_pendingKeys.pop_back();
+        m_pendingKeys.append(Key(Qt::Key_Backslash));
+        m_keys.append(Key(Qt::Key_Backslash));
+        goto accept;
+    }
+
     // 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) {
     case Qt::Key_0:
     {
-        if (modifiers == Qt::NoModifier) {
+        if (modifiers == Qt::NoModifier
+            || modifiers == Qt::KeypadModifier) {
             if (!m_keys.isEmpty()) {
                 // Repeat.
                 V_ASSERT(m_keys.last().isDigit());
@@ -376,7 +501,8 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
     case Qt::Key_8:
     case Qt::Key_9:
     {
-        if (modifiers == Qt::NoModifier) {
+        if (modifiers == Qt::NoModifier
+            || modifiers == Qt::KeypadModifier) {
             if (!m_keys.isEmpty() && numberFromKeySequence(m_keys) == -1) {
                 // Invalid sequence.
                 break;
@@ -475,8 +601,13 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
             }
 
             // Enter Insert mode.
-            if (m_mode == VimMode::Normal) {
-                setMode(VimMode::Insert);
+            // Different from Vim:
+            // We enter Insert mode even in Visual and VisualLine mode. We
+            // also keep the selection after the mode change.
+            if (checkMode(VimMode::Normal)
+                || checkMode(VimMode::Visual)
+                || checkMode(VimMode::VisualLine)) {
+                setMode(VimMode::Insert, false);
             }
         } else if (modifiers == Qt::ShiftModifier) {
             QTextCursor cursor = m_editor->textCursor();
@@ -1045,7 +1176,8 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
     {
         if (modifiers == Qt::ShiftModifier) {
             // Specify a register.
-            if (!m_keys.isEmpty() || !m_tokens.isEmpty()) {
+            tryGetRepeatToken(m_keys, m_tokens);
+            if (!m_keys.isEmpty() || hasActionToken()) {
                 // Invalid sequence.
                 break;
             }
@@ -1355,6 +1487,54 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
         break;
     }
 
+    case Qt::Key_Colon:
+    {
+        if (modifiers == Qt::ShiftModifier) {
+            if (m_keys.isEmpty()
+                && m_tokens.isEmpty()
+                && checkMode(VimMode::Normal)) {
+                // :, enter command line mode.
+                // For simplicity, we do not use a standalone mode for this mode.
+                // Just let it be in Normal mode and use another variable to
+                // specify this.
+                m_cmdMode = true;
+                goto accept;
+            }
+
+            break;
+        }
+
+        break;
+    }
+
+    case Qt::Key_Percent:
+    {
+        if (modifiers == Qt::ShiftModifier) {
+            tryGetRepeatToken(m_keys, m_tokens);
+            if (m_keys.isEmpty() && hasRepeatToken()) {
+                // 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);
+                if (bn == -1) {
+                    break;
+                } else {
+                    // Repeat of LineJump is based on 1.
+                    token->m_repeat = bn + 1;
+                }
+
+                tryAddMoveAction();
+                addMovementToken(Movement::LineJump);
+                processCommand(m_tokens);
+                break;
+            }
+
+            break;
+        }
+
+        break;
+    }
+
     default:
         break;
     }
@@ -1363,7 +1543,6 @@ clear_accept:
     resetState();
 
 accept:
-    p_event->accept();
     ret = true;
 
 exit:
@@ -1379,6 +1558,7 @@ void VVim::resetState()
     m_pendingKeys.clear();
     setRegister(c_unnamedRegister);
     m_resetPositionInBlock = true;
+    m_cmdMode = false;
 }
 
 VimMode VVim::getMode() const
@@ -1386,10 +1566,13 @@ VimMode VVim::getMode() const
     return m_mode;
 }
 
-void VVim::setMode(VimMode p_mode)
+void VVim::setMode(VimMode p_mode, bool p_clearSelection)
 {
     if (m_mode != p_mode) {
-        clearSelection();
+        if (p_clearSelection) {
+            clearSelection();
+        }
+
         m_mode = p_mode;
         resetState();
 
@@ -1572,69 +1755,6 @@ void VVim::processMoveAction(QList<Token> &p_tokens)
     }
 }
 
-#define ADDKEY(x, y) case (x): {ch = (y); break;}
-
-// Returns NULL QChar if invalid.
-static QChar keyToChar(int p_key, int p_modifiers)
-{
-    if (p_modifiers == Qt::ControlModifier) {
-        return QChar();
-    }
-
-    if (p_key >= Qt::Key_0 && p_key <= Qt::Key_9) {
-        return QChar('0' + (p_key - Qt::Key_0));
-    } else if (p_key >= Qt::Key_A && p_key <= Qt::Key_Z) {
-        if (p_modifiers == Qt::ShiftModifier) {
-            return QChar('A' + (p_key - Qt::Key_A));
-        } else {
-            return QChar('a' + (p_key - Qt::Key_A));
-        }
-    }
-
-    QChar ch;
-    switch (p_key) {
-    ADDKEY(Qt::Key_Tab, '\t');
-    ADDKEY(Qt::Key_Space, ' ');
-    ADDKEY(Qt::Key_Exclam, '!');
-    ADDKEY(Qt::Key_QuoteDbl, '"');
-    ADDKEY(Qt::Key_NumberSign, '#');
-    ADDKEY(Qt::Key_Dollar, '$');
-    ADDKEY(Qt::Key_Percent, '%');
-    ADDKEY(Qt::Key_Ampersand, '&');
-    ADDKEY(Qt::Key_Apostrophe, '\'');
-    ADDKEY(Qt::Key_ParenLeft, '(');
-    ADDKEY(Qt::Key_ParenRight, ')');
-    ADDKEY(Qt::Key_Asterisk, '*');
-    ADDKEY(Qt::Key_Plus, '+');
-    ADDKEY(Qt::Key_Comma, ',');
-    ADDKEY(Qt::Key_Minus, '-');
-    ADDKEY(Qt::Key_Period, '.');
-    ADDKEY(Qt::Key_Slash, '/');
-    ADDKEY(Qt::Key_Colon, ':');
-    ADDKEY(Qt::Key_Semicolon, ';');
-    ADDKEY(Qt::Key_Less, '<');
-    ADDKEY(Qt::Key_Equal, '=');
-    ADDKEY(Qt::Key_Greater, '>');
-    ADDKEY(Qt::Key_Question, '?');
-    ADDKEY(Qt::Key_At, '@');
-    ADDKEY(Qt::Key_BracketLeft, '[');
-    ADDKEY(Qt::Key_Backslash, '\\');
-    ADDKEY(Qt::Key_BracketRight, ']');
-    ADDKEY(Qt::Key_AsciiCircum, '^');
-    ADDKEY(Qt::Key_Underscore, '_');
-    ADDKEY(Qt::Key_QuoteLeft, '`');
-    ADDKEY(Qt::Key_BraceLeft, '{');
-    ADDKEY(Qt::Key_Bar, '|');
-    ADDKEY(Qt::Key_BraceRight, '}');
-    ADDKEY(Qt::Key_AsciiTilde, '~');
-
-    default:
-        break;
-    }
-
-    return ch;
-}
-
 bool VVim::processMovement(QTextCursor &p_cursor, const QTextDocument *p_doc,
                            QTextCursor::MoveMode p_moveMode,
                            const Token &p_token, int p_repeat)
@@ -3247,6 +3367,20 @@ bool VVim::expectingCharacterTarget() const
             || key == Key(Qt::Key_T, Qt::ShiftModifier));
 }
 
+bool VVim::expectingCommandLineInput() const
+{
+    return m_cmdMode;
+}
+
+bool VVim::expectingLeaderSequence() const
+{
+    if (m_replayLeaderSequence || m_keys.isEmpty()) {
+        return false;
+    }
+
+    return m_keys.first() == Key(Qt::Key_Backslash);
+}
+
 QChar VVim::keyToRegisterName(const Key &p_key) const
 {
     if (p_key.isAlphabet()) {
@@ -3301,6 +3435,24 @@ bool VVim::hasActionToken() const
     return has;
 }
 
+bool VVim::hasRepeatToken() const
+{
+    // There will be only one repeat token.
+    bool has = false;
+    if (m_tokens.isEmpty()) {
+        return false;
+    }
+
+    for (int i = 0; i < m_tokens.size(); ++i) {
+        if (m_tokens.at(i).isRepeat()) {
+            V_ASSERT(!has);
+            has = true;
+        }
+    }
+
+    return has;
+}
+
 bool VVim::hasActionTokenValidForTextObject() const
 {
     if (hasActionToken()) {
@@ -3350,6 +3502,19 @@ const VVim::Token *VVim::getActionToken() const
     return &m_tokens.first();
 }
 
+VVim::Token *VVim::getRepeatToken()
+{
+    V_ASSERT(hasRepeatToken());
+
+    for (auto & token : m_tokens) {
+        if (token.isRepeat()) {
+            return &token;
+        }
+    }
+
+    return NULL;
+}
+
 void VVim::addRangeToken(Range p_range)
 {
     m_tokens.append(Token(p_range));
@@ -3576,3 +3741,192 @@ void VVim::setRegister(QChar p_reg)
 {
     m_regName = p_reg;
 }
+
+bool VVim::checkMode(VimMode p_mode)
+{
+    return m_mode == p_mode;
+}
+
+bool VVim::processCommandLine(const Key &p_key)
+{
+    Q_ASSERT(m_cmdMode);
+
+    if (p_key == Key(Qt::Key_Return)
+        || p_key == Key(Qt::Key_Enter, Qt::KeypadModifier)) {
+        // Enter, try to execute the command and exit cmd line mode.
+        executeCommand(m_keys);
+        m_cmdMode = false;
+        return true;
+    }
+
+    if (p_key.m_key == Qt::Key_Escape
+        || (p_key.m_key == Qt::Key_BracketLeft && p_key.m_modifiers == Qt::ControlModifier)) {
+        // Go back to Normal mode.
+        m_keys.clear();
+        m_pendingKeys.clear();
+        m_cmdMode = false;
+
+        setMode(VimMode::Normal);
+        return true;
+    }
+
+    switch (p_key.m_key) {
+    case Qt::Key_Backspace:
+        // Delete one char backward.
+        if (m_keys.isEmpty()) {
+            // Exit command line mode.
+            Q_ASSERT(m_pendingKeys.size() == 1);
+            m_pendingKeys.pop_back();
+            m_cmdMode = false;
+            return true;
+        } else {
+            m_keys.pop_back();
+            m_pendingKeys.pop_back();
+        }
+
+        break;
+
+    case Qt::Key_U:
+    {
+        if (p_key.m_modifiers == Qt::ControlModifier) {
+            // Ctrl+U, delete all input keys.
+            while (!m_keys.isEmpty()) {
+                m_keys.pop_back();
+                m_pendingKeys.pop_back();
+            }
+        } else {
+            // Just pend this key.
+            m_pendingKeys.append(p_key);
+            m_keys.append(p_key);
+        }
+
+        break;
+    }
+
+    default:
+        // Just pend this key.
+        m_pendingKeys.append(p_key);
+        m_keys.append(p_key);
+    }
+
+    return false;
+}
+
+void VVim::executeCommand(const QList<Key> &p_keys)
+{
+    bool validCommand = true;
+    QString msg;
+
+    if (p_keys.isEmpty()) {
+        return;
+    } if (p_keys.size() == 1) {
+        const Key &key0 = p_keys.first();
+        if (key0 == Key(Qt::Key_W)) {
+            // :w, save current file.
+            emit m_editor->saveNote();
+            msg = tr("Note has been saved");
+        } else if (key0 == Key(Qt::Key_Q)) {
+            // :q, quit edit mode.
+            emit m_editor->discardAndRead();
+            msg = tr("Quit");
+        } else if (key0 == Key(Qt::Key_X)) {
+            // :x, save if there is any change and quit edit mode.
+            emit m_editor->saveAndRead();
+            msg = tr("Quit with note having been saved");
+        } else {
+            validCommand = false;
+        }
+    } else if (p_keys.size() == 2) {
+        const Key &key0 = p_keys.first();
+        const Key &key1 = p_keys.at(1);
+        if (key0 == Key(Qt::Key_W) && key1 == Key(Qt::Key_Q)) {
+            // :wq, save change and quit edit mode.
+            // We treat it same as :x.
+            emit m_editor->saveAndRead();
+            msg = tr("Quit with note having been saved");
+        } else if (key0 == Key(Qt::Key_Q) && key1 == Key(Qt::Key_Exclam, Qt::ShiftModifier)) {
+            // :q!, discard change and quit edit mode.
+            emit m_editor->discardAndRead();
+            msg = tr("Quit");
+        } else {
+            validCommand = false;
+        }
+    } else {
+        validCommand = false;
+    }
+
+    if (!validCommand) {
+        QString str;
+        for (auto const & key : p_keys) {
+            str.append(keyToChar(key.m_key, key.m_modifiers));
+        }
+
+        message(tr("Not an editor command: %1").arg(str));
+    } else {
+        message(msg);
+    }
+}
+
+bool VVim::hasNonDigitPendingKeys()
+{
+    for (auto const &key : m_keys) {
+        if (!key.isDigit()) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+bool VVim::processLeaderSequence(const Key &p_key)
+{
+    // Different from Vim:
+    // If it is not a valid sequence, we just do nothing here.
+    V_ASSERT(checkPendingKey(Key(Qt::Key_Backslash)));
+    bool validSequence = true;
+    QList<Key> replaySeq;
+
+    if (p_key == Key(Qt::Key_Y)) {
+        // <leader>y, "+y
+        replaySeq.append(Key(Qt::Key_QuoteDbl, Qt::ShiftModifier));
+        replaySeq.append(Key(Qt::Key_Plus, Qt::ShiftModifier));
+        replaySeq.append(Key(Qt::Key_Y));
+    } else if (p_key == Key(Qt::Key_D)) {
+        // <leader>d, "+d
+        replaySeq.append(Key(Qt::Key_QuoteDbl, Qt::ShiftModifier));
+        replaySeq.append(Key(Qt::Key_Plus, Qt::ShiftModifier));
+        replaySeq.append(Key(Qt::Key_D));
+    } else if (p_key == Key(Qt::Key_P)) {
+        // <leader>p, "+p
+        replaySeq.append(Key(Qt::Key_QuoteDbl, Qt::ShiftModifier));
+        replaySeq.append(Key(Qt::Key_Plus, Qt::ShiftModifier));
+        replaySeq.append(Key(Qt::Key_P));
+    } else if (p_key == Key(Qt::Key_P, Qt::ShiftModifier)) {
+        // <leader>P, "+P
+        replaySeq.append(Key(Qt::Key_QuoteDbl, Qt::ShiftModifier));
+        replaySeq.append(Key(Qt::Key_Plus, Qt::ShiftModifier));
+        replaySeq.append(Key(Qt::Key_P, Qt::ShiftModifier));
+    } else {
+        validSequence = false;
+    }
+
+    if (!replaySeq.isEmpty()) {
+        // Replay the sequence.
+        m_replayLeaderSequence = true;
+        m_keys.clear();
+        for (int i = 0; i < 2; ++i) {
+            m_pendingKeys.pop_back();
+        }
+
+        for (auto const &key : replaySeq) {
+            bool ret = handleKeyPressEvent(key.m_key, key.m_modifiers);
+            if (!ret) {
+                break;
+            }
+        }
+
+        m_replayLeaderSequence = false;
+    }
+
+    return validSequence;
+}

+ 54 - 2
src/utils/vvim.h

@@ -99,7 +99,7 @@ public:
     VimMode getMode() const;
 
     // Set current mode.
-    void setMode(VimMode p_mode);
+    void setMode(VimMode p_mode, bool p_clearSelection = true);
 
     // Set current register.
     void setRegister(QChar p_reg);
@@ -146,7 +146,7 @@ private:
         {
             return m_key >= Qt::Key_0
                    && m_key <= Qt::Key_9
-                   && m_modifiers == Qt::NoModifier;
+                   && (m_modifiers == Qt::NoModifier || m_modifiers == Qt::KeypadModifier);
         }
 
         int toDigit() const
@@ -348,6 +348,9 @@ private:
         Key m_key;
     };
 
+    // Returns true if the event is consumed and need no more handling.
+    bool handleKeyPressEvent(int key, int modifiers);
+
     // Reset all key state info.
     void resetState();
 
@@ -416,6 +419,12 @@ private:
     // Check m_keys to see if we are expecting a target for f/t/F/T command.
     bool expectingCharacterTarget() const;
 
+    // Check if we are in command line mode.
+    bool expectingCommandLineInput() const;
+
+    // Check if we are in a leader sequence.
+    bool expectingLeaderSequence() const;
+
     // Return the corresponding register name of @p_key.
     // If @p_key is not a valid register name, return a NULL QChar.
     QChar keyToRegisterName(const Key &p_key) const;
@@ -423,6 +432,9 @@ private:
     // Check if @m_tokens contains an action token.
     bool hasActionToken() const;
 
+    // Check if @m_tokens contains a repeat token.
+    bool hasRepeatToken() const;
+
     // Try to add an Action::Move action at the front if there is no any action
     // token.
     void tryAddMoveAction();
@@ -433,6 +445,9 @@ private:
     // Get the action token from m_tokens.
     const Token *getActionToken() const;
 
+    // Get the repeat token from m_tokens.
+    Token *getRepeatToken();
+
     // Add an Range token at the end of m_tokens.
     void addRangeToken(Range p_range);
 
@@ -487,6 +502,32 @@ private:
 
     void message(const QString &p_str);
 
+    // Check if m_mode equals to p_mode.
+    bool checkMode(VimMode p_mode);
+
+    // In command line mode, read input @p_key and process it.
+    // Returns true if a command has been completed, otherwise returns false.
+    bool processCommandLine(const Key &p_key);
+
+    // Execute command specified by @p_keys.
+    // @p_keys does not contain the leading colon.
+    // Following commands are supported:
+    // :w, :wq, :q, :q!, :x
+    void executeCommand(const QList<Key> &p_keys);
+
+    // Check if m_keys has non-digit key.
+    bool hasNonDigitPendingKeys();
+
+    // Reading a leader sequence, read input @p_key and process it.
+    // Returns true if a sequence has been replayed or it is being read,
+    // otherwise returns false.
+    // Following sequences are supported:
+    // y: "+y
+    // d: "+d
+    // p: "+p
+    // P: "+P
+    bool processLeaderSequence(const Key &p_key);
+
     VEdit *m_editor;
     const VEditConfig *m_editConfig;
     VimMode m_mode;
@@ -512,6 +553,17 @@ private:
     // Last f/F/t/T Token.
     Token m_lastFindToken;
 
+    // Whether in command line mode.
+    bool m_cmdMode;
+
+    // The leader key, which is Key_Space by default.
+    Key m_leaderKey;
+
+    // Whether we are parsing a leader sequence.
+    // We will map a leader sequence to another actual sequence. When replaying
+    // this actual sequence, m_leaderSequence will be true.
+    bool m_replayLeaderSequence;
+
     static const QChar c_unnamedRegister;
     static const QChar c_blackHoleRegister;
     static const QChar c_selectionRegister;

+ 8 - 0
src/vedit.h

@@ -86,10 +86,18 @@ public:
     void requestUpdateVimStatus();
 
 signals:
+    // 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();
+
     // Emit when m_config has been updated.
     void configUpdated();
 

+ 8 - 2
src/vmdeditoperations.cpp

@@ -311,6 +311,8 @@ bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event)
         break;
     }
 
+    case Qt::Key_Enter:
+        // Fall through.
     case Qt::Key_Return:
     {
         if (handleKeyReturn(p_event)) {
@@ -326,8 +328,12 @@ bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event)
 
 exit:
     // Qt::Key_Return, Qt::Key_Tab and Qt::Key_Backtab will handle m_autoIndentPos.
-    if (key != Qt::Key_Return && key != Qt::Key_Tab && key != Qt::Key_Backtab &&
-        key != Qt::Key_Shift) {
+    if (key != Qt::Key_Return
+        && key != Qt::Key_Enter
+        && key != Qt::Key_Tab
+        && key != Qt::Key_Backtab
+        && key != Qt::Key_Shift
+        && key != Qt::Key_Control) {
         m_autoIndentPos = -1;
     }
 

+ 2 - 0
src/vmdtab.cpp

@@ -58,6 +58,8 @@ void VMdTab::setupUI()
                 this, &VMdTab::saveAndRead);
         connect(m_editor, &VEdit::discardAndRead,
                 this, &VMdTab::discardAndRead);
+        connect(m_editor, &VEdit::saveNote,
+                this, &VMdTab::saveFile);
         connect(m_editor, &VEdit::statusMessage,
                 this, &VEditTab::statusMessage);
         connect(m_editor, &VEdit::vimStatusUpdated,