Browse Source

vim-mode: refine command line mode

1. Support `/` and `?` to search. `N` and `Shift+N` to find next/previous
occurence.
2. `Ctrl+N` and `Ctrl+Shift+N` to navigate through the command history.
3. `:nohlsearch` or `<leader><space>` to clear search highlight.
4. `#` and `*` to search current word under cursor.
Le Tan 8 years ago
parent
commit
cd2ac10509

+ 0 - 8
src/dialog/vfindreplacedialog.h

@@ -9,14 +9,6 @@ class QLineEdit;
 class QPushButton;
 class QCheckBox;
 
-enum FindOption
-{
-    CaseSensitive = 0x1U,
-    WholeWordOnly = 0x2U,
-    RegularExpression = 0x4U,
-    IncrementalSearch = 0x8U
-};
-
 class VFindReplaceDialog : public QWidget
 {
     Q_OBJECT

+ 6 - 2
src/resources/docs/shortcuts_en.md

@@ -5,7 +5,7 @@
 ## Normal Shortcuts
 - `Ctrl+E E`  
 Toggle expanding the edit area.
-- `Ctrl+N`  
+- `Ctrl+Alt+N`  
 Create a note in current directory.
 - `Ctrl+F`  
 Find/Replace in current note.
@@ -162,10 +162,11 @@ VNote supports following features of Vim:
 - Jump locations list (`Ctrl+O` and `Ctrl+I`);
 - Leader key (`Space`)
     - Currently `<leader>y/d/p` equals to `"+y/d/p`, which will access the system's clipboard;
+    - `<leader><Space>` to clear search highlight;
 - `zz`, `zb`, `zt`;
 - `u` and `Ctrl+R` for undo and redo;
 - Text objects `i/a`: word, WORD, `''`, `""`, `` ` ` ``, `()`, `[]`, `<>`, and `{}`;
-- Command line `:w`, `:wq`, `:x`, `:q`, and `:q!`;
+- Command line `:w`, `:wq`, `:x`, `:q`, `:q!`, and `:nohlsearch`;
 - Jump between titles
     - `[[`: jump to previous title;
     - `]]`: jump to next title;
@@ -173,6 +174,9 @@ VNote supports following features of Vim:
     - `][`: jump to next title at the same level;
     - `[{`: jump to previous title at a higher level;
     - `]}`: jump to next title at a higher level;
+- `/` and `?` to search
+    - `n` and `N` to find next or previous occurence;
+    - `Ctrl+N` and `Ctrl+P` to navigate through the search history;
 
 For now, VNote does **NOT** support the macro and repeat(`.`) features of Vim.
 

+ 7 - 3
src/resources/docs/shortcuts_zh.md

@@ -5,7 +5,7 @@
 ## 常规快捷键
 - `Ctrl+E E`  
 是否扩展编辑区域。
-- `Ctrl+N`  
+- `Ctrl+Alt+N`  
 在当前文件夹下新建笔记。
 - `Ctrl+F`  
 页内查找和替换。
@@ -163,10 +163,11 @@ VNote支持以下几个Vim的特性:
 - 跳转位置列表 (`Ctrl+O` and `Ctrl+I`);
 - 前导键 (`Space`)
     - 目前 `<leader>y/d/p` 等同于 `"+y/d/p`, 从而可以访问系统剪切板;
+    - `<leader><Space>` 清除查找高亮;
 - `zz`, `zb`, `zt`;
 - `u` 和 `Ctrl+R` 撤销和重做;
-- 文本对象 `i/a`:word, WORD, `''`, `""`, `` ` ` ``, `()`, `[]`, `<>`, and `{}`;
-- 命令行 `:w`, `:wq`, `:x`, `:q`, and `:q!`;
+- 文本对象 `i/a`:word, WORD, `''`, `""`, `` ` ` ``, `()`, `[]`, `<>`, `{}`;
+- 命令行 `:w`, `:wq`, `:x`, `:q`, `:q!`, `:nohlsearch`;
 - 标题跳转
     - `[[`:跳转到上一个标题;
     - `]]`: 跳转到下一个标题;
@@ -174,6 +175,9 @@ VNote支持以下几个Vim的特性:
     - `][`:跳转到下一个同层级的标题;
     - `[{`:跳转到上一个高一层级的标题;
     - `]}`:跳转到下一个高一层级的标题;
+- `/` 和 `?` 开始查找
+    - `n` 和 `N` 查找下一处或上一处;
+    - `Ctrl+N` 和 `Ctrl+P` 浏览查找历史;
 
 VNote目前暂时不支持Vim的宏和重复(`.`)特性。
 

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

@@ -12,11 +12,20 @@ editor
 # QTextEdit just choose the first available font, so specify the Chinese fonts first
 # Do not use "" to quote the name
 font-family: Hiragino Sans GB, 冬青黑体, Microsoft YaHei, 微软雅黑, Microsoft YaHei UI, WenQuanYi Micro Hei, 文泉驿雅黑, Dengxian, 等线体, STXihei, 华文细黑, Liberation Sans, Droid Sans, NSimSun, 新宋体, SimSun, 宋体, Helvetica, sans-serif, Tahoma, Arial, Verdana, Geneva, Georgia, Times New Roman
+font-size: 12
 # [VNote] Style for trailing space
 trailing-space: a8a8a8
-font-size: 12
+# [VNote] Style for line number
 line-number-background: bdbdbd
 line-number-foreground: 424242
+# [VNote] style for selected word highlight
+selected-word-background: dfdf00
+# [VNote] style for searched word highlight
+searched-word-background: 4db6ac
+# [VNote] style for searched word under cursor highlight
+searched-word-cursor-background: 66bb6a
+# [VNote] style for incremental searched word highlight
+incremental-searched-word-background: ce93d8
 
 editor-selection
 foreground: eeeeee

+ 1 - 1
src/resources/vnote.ini

@@ -78,7 +78,7 @@ size=4
 ; Ctrl+E is reserved for Captain Mode.
 ; Ctrl+Q is reserved for quitting VNote.
 1\operation=NewNote
-1\keysequence=Ctrl+N
+1\keysequence=Ctrl+Alt+N
 2\operation=SaveNote
 2\keysequence=Ctrl+S
 3\operation=SaveAndRead

+ 389 - 102
src/utils/vvim.cpp

@@ -11,6 +11,7 @@
 #include "vconfigmanager.h"
 #include "vedit.h"
 #include "utils/veditutils.h"
+#include "vconstants.h"
 
 extern VConfigManager vconfig;
 
@@ -18,6 +19,8 @@ const QChar VVim::c_unnamedRegister = QChar('"');
 const QChar VVim::c_blackHoleRegister = QChar('_');
 const QChar VVim::c_selectionRegister = QChar('+');
 
+const int VVim::SearchHistory::c_capacity = 50;
+
 #define ADDKEY(x, y) case (x): {ch = (y); break;}
 
 // Returns NULL QChar if invalid.
@@ -86,7 +89,7 @@ VVim::VVim(VEdit *p_editor)
     : QObject(p_editor), m_editor(p_editor),
       m_editConfig(&p_editor->getConfig()), m_mode(VimMode::Invalid),
       m_resetPositionInBlock(true), m_regName(c_unnamedRegister),
-      m_cmdMode(false), m_leaderKey(Key(Qt::Key_Space)), m_replayLeaderSequence(false)
+      m_leaderKey(Key(Qt::Key_Space)), m_replayLeaderSequence(false)
 {
     Q_ASSERT(m_editConfig->m_enableVimMode);
 
@@ -496,16 +499,6 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
         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()) {
@@ -726,8 +719,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
 
             V_ASSERT(mm != Movement::Invalid);
             tryAddMoveAction();
-
-            m_tokens.append(Token(mm));
+            addMovementToken(mm);
             processCommand(m_tokens);
             resetPositionInBlock = false;
         }
@@ -1784,15 +1776,8 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
             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;
+                emit commandLineTriggered(CommandLineType::Command);
             }
-
-            break;
         }
 
         break;
@@ -2003,6 +1988,91 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
         break;
     }
 
+    case Qt::Key_Slash:
+    {
+        if (modifiers == Qt::NoModifier) {
+            if (m_tokens.isEmpty()
+                && m_keys.isEmpty()
+                && checkMode(VimMode::Normal)) {
+                emit commandLineTriggered(CommandLineType::SearchForward);
+            }
+        }
+
+        break;
+    }
+
+    case Qt::Key_Question:
+    {
+        if (modifiers == Qt::ShiftModifier) {
+            if (m_tokens.isEmpty()
+                && m_keys.isEmpty()
+                && checkMode(VimMode::Normal)) {
+                emit commandLineTriggered(CommandLineType::SearchBackward);
+            }
+        }
+
+        break;
+    }
+
+    case Qt::Key_N:
+    {
+        if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) {
+            // n, FindNext/FindPrevious movement.
+            tryGetRepeatToken(m_keys, m_tokens);
+
+            if (!m_keys.isEmpty()) {
+                break;
+            }
+
+            Movement mm = Movement::FindNext;
+            if (modifiers == Qt::ShiftModifier) {
+                mm = Movement::FindPrevious;
+            }
+
+            tryAddMoveAction();
+            addMovementToken(mm);
+            processCommand(m_tokens);
+        }
+
+        break;
+    }
+
+    case Qt::Key_Asterisk:
+    {
+        if (modifiers == Qt::ShiftModifier) {
+            // *, FindNextWordUnderCursor movement.
+            tryGetRepeatToken(m_keys, m_tokens);
+
+            if (!m_keys.isEmpty()) {
+                break;
+            }
+
+            tryAddMoveAction();
+            addMovementToken(Movement::FindNextWordUnderCursor);
+            processCommand(m_tokens);
+        }
+
+        break;
+    }
+
+    case Qt::Key_NumberSign:
+    {
+        if (modifiers == Qt::ShiftModifier) {
+            // #, FindPreviousWordUnderCursor movement.
+            tryGetRepeatToken(m_keys, m_tokens);
+
+            if (!m_keys.isEmpty()) {
+                break;
+            }
+
+            tryAddMoveAction();
+            addMovementToken(Movement::FindPreviousWordUnderCursor);
+            processCommand(m_tokens);
+        }
+
+        break;
+    }
+
     default:
         break;
     }
@@ -2031,7 +2101,6 @@ void VVim::resetState()
     m_pendingKeys.clear();
     setRegister(c_unnamedRegister);
     m_resetPositionInBlock = true;
-    m_cmdMode = false;
 }
 
 VimMode VVim::getMode() const
@@ -2808,6 +2877,99 @@ handle_target:
         break;
     }
 
+    case Movement::FindPrevious:
+        forward = false;
+        // Fall through.
+    case Movement::FindNext:
+    {
+        if (p_repeat == -1) {
+            p_repeat = 1;
+        }
+
+        if (m_searchHistory.isEmpty()) {
+            break;
+        }
+
+        // Record current location.
+        m_locations.addLocation(p_cursor);
+
+        const SearchItem &item = m_searchHistory.lastItem();
+        while (--p_repeat >= 0) {
+            hasMoved = m_editor->findText(item.m_text, item.m_options,
+                                          forward ? item.m_forward : !item.m_forward,
+                                          &p_cursor, p_moveMode);
+        }
+
+        break;
+    }
+
+    case Movement::FindPreviousWordUnderCursor:
+        forward = false;
+        // Fall through.
+    case Movement::FindNextWordUnderCursor:
+    {
+        if (p_repeat == -1) {
+            p_repeat = 1;
+        }
+
+        // Get current word under cursor.
+        // Different from Vim:
+        // We do not recognize a word as strict as Vim.
+        int start, end;
+        findCurrentWord(p_cursor, start, end);
+        if (start == end) {
+            // Spaces, find next word.
+            QTextCursor cursor = p_cursor;
+            while (true) {
+                moveCursorAcrossSpaces(cursor, p_moveMode, true);
+                if (cursor.atEnd()) {
+                    break;
+                }
+
+                if (!doc->characterAt(cursor.position()).isSpace()) {
+                    findCurrentWord(cursor, start, end);
+                    Q_ASSERT(start != end);
+                    break;
+                }
+            }
+
+            if (start == end) {
+                break;
+            }
+        }
+
+        QTextCursor cursor = p_cursor;
+        cursor.setPosition(start);
+        cursor.setPosition(end, QTextCursor::KeepAnchor);
+        QString text = cursor.selectedText();
+        if (text.isEmpty()) {
+            break;
+        }
+
+        // Record current location.
+        m_locations.addLocation(p_cursor);
+
+        p_cursor.setPosition(start, p_moveMode);
+
+        // Case-insensitive, non-regularexpression.
+        SearchItem item;
+        item.m_rawStr = text;
+        item.m_text = text;
+        item.m_forward = forward;
+
+        m_searchHistory.addItem(item);
+        m_searchHistory.resetIndex();
+        while (--p_repeat >= 0) {
+            hasMoved = m_editor->findText(item.m_text, item.m_options,
+                                          item.m_forward,
+                                          &p_cursor, p_moveMode);
+        }
+
+        Q_ASSERT(hasMoved);
+
+        break;
+    }
+
     default:
         break;
     }
@@ -4395,11 +4557,6 @@ bool VVim::expectingReplaceCharacter() const
            && m_keys.first() == Key(Qt::Key_R, Qt::NoModifier);
 }
 
-bool VVim::expectingCommandLineInput() const
-{
-    return m_cmdMode;
-}
-
 bool VVim::expectingLeaderSequence() const
 {
     if (m_replayLeaderSequence || m_keys.isEmpty()) {
@@ -4800,134 +4957,153 @@ bool VVim::checkMode(VimMode p_mode)
     return m_mode == p_mode;
 }
 
-bool VVim::processCommandLine(const Key &p_key)
+bool VVim::processCommandLine(VVim::CommandLineType p_type, const QString &p_cmd)
 {
-    Q_ASSERT(m_cmdMode);
+    setMode(VimMode::Normal);
 
-    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_cmdMode = false;
-        return true;
+    bool ret = false;
+    switch (p_type) {
+    case CommandLineType::Command:
+        ret = executeCommand(p_cmd);
+        break;
+
+    case CommandLineType::SearchForward:
+        // Fall through.
+    case CommandLineType::SearchBackward:
+    {
+        SearchItem item = fetchSearchItem(p_type, p_cmd);
+        m_editor->findText(item.m_text, item.m_options, item.m_forward);
+        m_searchHistory.addItem(item);
+        m_searchHistory.resetIndex();
+        break;
     }
 
-    if (p_key.m_key == Qt::Key_Escape
-        || (p_key.m_key == Qt::Key_BracketLeft && isControlModifier(p_key.m_modifiers))) {
-        // Go back to Normal mode.
-        m_keys.clear();
-        m_pendingKeys.clear();
-        m_cmdMode = false;
+    default:
+        break;
+    }
 
-        setMode(VimMode::Normal);
-        return true;
+    return ret;
+}
+
+void VVim::processCommandLineChanged(VVim::CommandLineType p_type,
+                                     const QString &p_cmd)
+{
+    setMode(VimMode::Normal);
+
+    if (p_type == CommandLineType::SearchForward
+        || p_type == CommandLineType::SearchBackward) {
+        // Peek text.
+        SearchItem item = fetchSearchItem(p_type, p_cmd);
+        m_editor->peekText(item.m_text, item.m_options, item.m_forward);
     }
+}
 
-    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();
-        }
+void VVim::processCommandLineCancelled()
+{
+    m_searchHistory.resetIndex();
+    m_editor->clearIncrementalSearchedWordHighlight();
+}
 
-        break;
+VVim::SearchItem VVim::fetchSearchItem(VVim::CommandLineType p_type,
+                                       const QString &p_cmd)
+{
+    Q_ASSERT(p_type == CommandLineType::SearchForward
+             || p_type == CommandLineType::SearchBackward);
 
-    case Qt::Key_U:
-    {
-        if (isControlModifier(p_key.m_modifiers)) {
-            // 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);
-        }
+    SearchItem item;
+    item.m_rawStr = p_cmd;
+    item.m_text = p_cmd;
+    item.m_forward = p_type == CommandLineType::SearchForward;
 
-        break;
+    if (p_cmd.indexOf("\\C") > -1) {
+        item.m_options |= FindOption::CaseSensitive;
+        item.m_text.remove("\\C");
     }
 
-    default:
-        // Just pend this key.
-        m_pendingKeys.append(p_key);
-        m_keys.append(p_key);
-    }
+    item.m_options |= FindOption::RegularExpression;
 
-    return false;
+    return item;
 }
 
-void VVim::executeCommand()
+bool VVim::executeCommand(const QString &p_cmd)
 {
     bool validCommand = true;
     QString msg;
 
-    if (m_keys.isEmpty()) {
-        return;
-    } if (m_keys.size() == 1) {
-        const Key &key0 = m_keys.first();
-        if (key0 == Key(Qt::Key_W)) {
+    Q_ASSERT(m_tokens.isEmpty() && m_keys.isEmpty());
+    if (p_cmd.isEmpty()) {
+        return true;
+    }else if (p_cmd.size() == 1) {
+        if (p_cmd == "w") {
             // :w, save current file.
             emit m_editor->saveNote();
             msg = tr("Note has been saved");
-        } else if (key0 == Key(Qt::Key_Q)) {
+        } else if (p_cmd == "q") {
             // :q, quit edit mode.
             emit m_editor->discardAndRead();
             msg = tr("Quit");
-        } else if (key0 == Key(Qt::Key_X)) {
+        } else if (p_cmd == "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 (m_keys.size() == 2) {
-        const Key &key0 = m_keys.first();
-        const Key &key1 = m_keys.at(1);
-        if (key0 == Key(Qt::Key_W) && key1 == Key(Qt::Key_Q)) {
+    } else if (p_cmd.size() == 2) {
+        if (p_cmd == "wq") {
             // :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)) {
+        } else if (p_cmd == "q!") {
             // :q!, discard change and quit edit mode.
             emit m_editor->discardAndRead();
             msg = tr("Quit");
         } else {
             validCommand = false;
         }
+    } else if (p_cmd == "nohlsearch") {
+        // :nohlsearch, clear highlight search.
+        clearSearchHighlight();
     } else {
         validCommand = false;
     }
 
-    if (!validCommand && !hasNonDigitPendingKeys() && m_tokens.isEmpty()) {
+    if (!validCommand) {
+        bool allDigits = true;
+        for (int i = 0; i < p_cmd.size(); ++i) {
+            if (!p_cmd[i].isDigit()) {
+                allDigits = false;
+                break;
+            }
+        }
+
         // All digits.
         // Jump to a specific line.
-        tryGetRepeatToken(m_keys, m_tokens);
-        tryAddMoveAction();
-        addMovementToken(Movement::LineJump);
-        processCommand(m_tokens);
-        validCommand = true;
-    }
+        if (allDigits) {
+            bool ok;
+            int num = p_cmd.toInt(&ok, 10);
+            if (num == 0) {
+                num = 1;
+            }
 
-    if (!validCommand) {
-        QString str;
-        for (auto const & key : m_keys) {
-            str.append(keyToChar(key.m_key, key.m_modifiers));
+            if (ok && num > 0) {
+                m_tokens.append(Token(num));
+                tryAddMoveAction();
+                addMovementToken(Movement::LineJump);
+                processCommand(m_tokens);
+                validCommand = true;
+            }
         }
+    }
 
-        message(tr("Not an editor command: %1").arg(str));
+    if (!validCommand) {
+        message(tr("Not an editor command: %1").arg(p_cmd));
     } else {
         message(msg);
     }
+
+    return validCommand;
 }
 
 bool VVim::hasNonDigitPendingKeys(const QList<Key> &p_keys)
@@ -4974,6 +5150,9 @@ bool VVim::processLeaderSequence(const Key &p_key)
         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 if (p_key == Key(Qt::Key_Space)) {
+        // <leader><space>, clear search highlight
+        clearSearchHighlight();
     } else {
         validSequence = false;
     }
@@ -4994,6 +5173,8 @@ bool VVim::processLeaderSequence(const Key &p_key)
         }
 
         m_replayLeaderSequence = false;
+    } else {
+        resetState();
     }
 
     return validSequence;
@@ -5163,3 +5344,109 @@ void VVim::processTitleJump(const QList<Token> &p_tokens, bool p_forward, int p_
         m_locations.addLocation(cursor);
     }
 }
+
+void VVim::SearchHistory::addItem(const SearchItem &p_item)
+{
+    m_isLastItemForward = p_item.m_forward;
+    if (m_isLastItemForward) {
+        m_forwardItems.push_back(p_item);
+        m_forwardIdx = m_forwardItems.size();
+    } else {
+        m_backwardItems.push_back(p_item);
+        m_backwardIdx = m_forwardItems.size();
+    }
+
+    qDebug() << "search history add item" << m_isLastItemForward
+             << m_forwardIdx << m_forwardItems.size()
+             << m_backwardIdx << m_backwardItems.size();
+}
+
+const VVim::SearchItem &VVim::SearchHistory::lastItem() const
+{
+    if (m_isLastItemForward) {
+        Q_ASSERT(!m_forwardItems.isEmpty());
+        return m_forwardItems.back();
+    } else {
+        Q_ASSERT(!m_backwardItems.isEmpty());
+        return m_backwardItems.back();
+    }
+}
+
+const VVim::SearchItem &VVim::SearchHistory::nextItem(bool p_forward)
+{
+    Q_ASSERT(hasNext(p_forward));
+    return p_forward ? m_forwardItems.at(++m_forwardIdx)
+                     : m_backwardItems.at(++m_backwardIdx);
+}
+
+// Return previous item in the @p_forward stack.
+const VVim::SearchItem &VVim::SearchHistory::previousItem(bool p_forward)
+{
+    Q_ASSERT(hasPrevious(p_forward));
+    qDebug() << "previousItem" << p_forward << m_forwardItems.size() << m_backwardItems.size()
+             << m_forwardIdx << m_backwardIdx;
+    return p_forward ? m_forwardItems.at(--m_forwardIdx)
+                     : m_backwardItems.at(--m_backwardIdx);
+}
+
+void VVim::SearchHistory::resetIndex()
+{
+    m_forwardIdx = m_forwardItems.size();
+    m_backwardIdx = m_backwardItems.size();
+}
+
+QString VVim::getNextCommandHistory(VVim::CommandLineType p_type,
+                                    const QString &p_cmd)
+{
+    Q_UNUSED(p_cmd);
+    bool forward = false;
+    QString cmd;
+    switch (p_type) {
+    case CommandLineType::SearchForward:
+        forward = true;
+        // Fall through.
+    case CommandLineType::SearchBackward:
+        if (m_searchHistory.hasNext(forward)) {
+            return m_searchHistory.nextItem(forward).m_rawStr;
+        } else {
+            m_searchHistory.resetIndex();
+        }
+
+        break;
+
+    default:
+        break;
+    }
+
+    return cmd;
+}
+
+// Get the previous command in history of @p_type. @p_cmd is the current input.
+QString VVim::getPreviousCommandHistory(VVim::CommandLineType p_type,
+                                        const QString &p_cmd)
+{
+    Q_UNUSED(p_cmd);
+    bool forward = false;
+    QString cmd;
+    switch (p_type) {
+    case CommandLineType::SearchForward:
+        forward = true;
+        // Fall through.
+    case CommandLineType::SearchBackward:
+        if (m_searchHistory.hasPrevious(forward)) {
+            return m_searchHistory.previousItem(forward).m_rawStr;
+        }
+
+        break;
+
+    default:
+        break;
+    }
+
+    return cmd;
+}
+
+void VVim::clearSearchHighlight()
+{
+    m_editor->clearSearchedWordHighlight();
+}

+ 125 - 14
src/utils/vvim.h

@@ -146,6 +146,14 @@ public:
         QChar m_lastUsedMark;
     };
 
+    enum class CommandLineType
+    {
+        Command,
+        SearchForward,
+        SearchBackward,
+        Invalid
+    };
+
     // Handle key press event.
     // @p_autoIndentPos: the cursor position of last auto indent.
     // Returns true if the event is consumed and need no more handling.
@@ -172,6 +180,26 @@ public:
     // Get m_marks.
     const VVim::Marks &getMarks() const;
 
+    // Process command line of type @p_type and command @p_cmd.
+    // Returns true if it is a valid command.
+    bool processCommandLine(VVim::CommandLineType p_type, const QString &p_cmd);
+
+    // Process the command line text change.
+    void processCommandLineChanged(VVim::CommandLineType p_type,
+                                   const QString &p_cmd);
+
+    void processCommandLineCancelled();
+
+    // Get the next command in history of @p_type. @p_cmd is the current input.
+    // Return NULL QString if history is not applicable.
+    QString getNextCommandHistory(VVim::CommandLineType p_type,
+                                  const QString &p_cmd);
+
+    // Get the previous command in history of @p_type. @p_cmd is the current input.
+    // Return NULL QString if history is not applicable.
+    QString getPreviousCommandHistory(VVim::CommandLineType p_type,
+                                      const QString &p_cmd);
+
 signals:
     // Emit when current mode has been changed.
     void modeChanged(VimMode p_mode);
@@ -182,6 +210,9 @@ signals:
     // Emit when current status updated.
     void vimStatusUpdated(const VVim *p_vim);
 
+    // Emit when user pressed : to trigger command line.
+    void commandLineTriggered(VVim::CommandLineType p_type);
+
 private slots:
     // When user use mouse to select texts in Normal mode, we should change to
     // Visual mode.
@@ -242,6 +273,79 @@ private:
         }
     };
 
+    // Search item including the searched text and options.
+    struct SearchItem
+    {
+        SearchItem() : m_options(0), m_forward(true) {}
+
+        // The user raw input.
+        QString m_rawStr;
+
+        // The string used to search.
+        QString m_text;
+
+        uint m_options;
+        bool m_forward;
+    };
+
+    class SearchHistory
+    {
+    public:
+        SearchHistory()
+            : m_forwardIdx(0), m_backwardIdx(0), m_isLastItemForward(true) {}
+
+        // Add @p_item to history.
+        void addItem(const SearchItem &p_item);
+
+        // Whether the history is empty.
+        bool isEmpty() const
+        {
+            return m_forwardItems.isEmpty() && m_backwardItems.isEmpty();
+        }
+
+        bool hasNext(bool p_forward) const
+        {
+            return p_forward ? m_forwardIdx < m_forwardItems.size() - 1
+                             : m_backwardIdx < m_backwardItems.size() - 1;
+        }
+
+        bool hasPrevious(bool p_forward) const
+        {
+            return p_forward ? m_forwardIdx > 0
+                             : m_backwardIdx > 0;
+        }
+
+        // Return the last search item according to m_isLastItemForward.
+        // Make sure the history is not empty before calling this.
+        const SearchItem &lastItem() const;
+
+        // Return next item in the @p_forward stack.
+        // Make sure before by calling hasNext().
+        const SearchItem &nextItem(bool p_forward);
+
+        // Return previous item in the @p_forward stack.
+        // Make sure before by calling hasPrevious().
+        const SearchItem &previousItem(bool p_forward);
+
+        void resetIndex();
+
+    private:
+        // Maintain two stacks for the search history. Use the back as the top
+        // of the stack.
+        // The idx points to the next item to push.
+        // Just simply add new search item to the stack, without duplication.
+        QList<SearchItem> m_forwardItems;
+        int m_forwardIdx;
+
+        QList<SearchItem> m_backwardItems;
+        int m_backwardIdx;
+
+        // Whether last search item is forward or not.
+        bool m_isLastItemForward;
+
+        static const int c_capacity;
+    };
+
     // Supported actions.
     enum class Action
     {
@@ -301,6 +405,10 @@ private:
         MarkJump,
         MarkJumpLine,
         FindPair,
+        FindNext,
+        FindPrevious,
+        FindNextWordUnderCursor,
+        FindPreviousWordUnderCursor,
         Invalid
     };
 
@@ -549,9 +657,6 @@ private:
     // Check m_keys to see if we are expecting a character to replace with.
     bool expectingReplaceCharacter() const;
 
-    // Check if we are in command line mode.
-    bool expectingCommandLineInput() const;
-
     // Check if we are in a leader sequence.
     bool expectingLeaderSequence() const;
 
@@ -645,15 +750,12 @@ private:
     // 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 m_keys.
-    // @p_keys does not contain the leading colon.
+    // Execute command specified by @p_cmd.
+    // @p_cmd does not contain the leading colon.
+    // Returns true if it is a valid command.
     // Following commands are supported:
-    // :w, :wq, :q, :q!, :x
-    void executeCommand();
+    // w, wq, q, q!, x, <nums>
+    bool executeCommand(const QString &p_cmd);
 
     // Check if m_keys has non-digit key.
     bool hasNonDigitPendingKeys();
@@ -674,6 +776,15 @@ private:
     // [[, ]], [], ][, [{, ]}.
     void processTitleJump(const QList<Token> &p_tokens, bool p_forward, int p_relativeLevel);
 
+    // Fetch the searched string and options from @p_type and @p_cmd.
+    // \C for case-sensitive;
+    // Case-insensitive by default.
+    // Regular-expression by default.
+    VVim::SearchItem fetchSearchItem(VVim::CommandLineType p_type, const QString &p_cmd);
+
+    // Clear search highlight.
+    void clearSearchHighlight();
+
     VEdit *m_editor;
     const VEditConfig *m_editConfig;
     VimMode m_mode;
@@ -699,9 +810,6 @@ 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;
 
@@ -714,6 +822,9 @@ private:
 
     Marks m_marks;
 
+    // Search history.
+    SearchHistory m_searchHistory;
+
     static const QChar c_unnamedRegister;
     static const QChar c_blackHoleRegister;
     static const QChar c_selectionRegister;

+ 30 - 2
src/vconfigmanager.cpp

@@ -340,7 +340,11 @@ void VConfigManager::updateMarkdownEditStyle()
     static const QString defaultVimInsertBg = "#CDC0B0";
     static const QString defaultVimVisualBg = "#90CAF9";
     static const QString defaultVimReplaceBg = "#F8BBD0";
-    static const QString defaultTrailingSpaceBackground = "#A8A8A8";
+    static const QString defaultTrailingSpaceBg = "#A8A8A8";
+    static const QString defaultSelectedWordBg = "#DFDF00";
+    static const QString defaultSearchedWordBg = "#81C784";
+    static const QString defaultSearchedWordCursorBg = "#4DB6AC";
+    static const QString defaultIncrementalSearchedWordBg = "#CE93D8";
     static const QString defaultLineNumberBg = "#BDBDBD";
     static const QString defaultLineNumberFg = "#424242";
 
@@ -398,7 +402,11 @@ void VConfigManager::updateMarkdownEditStyle()
         }
     }
 
-    m_editorTrailingSpaceBg = defaultTrailingSpaceBackground;
+    m_editorTrailingSpaceBg = defaultTrailingSpaceBg;
+    m_editorSelectedWordBg = defaultSelectedWordBg;
+    m_editorSearchedWordBg = defaultSearchedWordBg;
+    m_editorSearchedWordCursorBg = defaultSearchedWordCursorBg;
+    m_editorIncrementalSearchedWordBg = defaultIncrementalSearchedWordBg;
     m_editorLineNumberBg = defaultLineNumberBg;
     m_editorLineNumberFg = defaultLineNumberFg;
     auto editorIt = styles.find("editor");
@@ -417,6 +425,26 @@ void VConfigManager::updateMarkdownEditStyle()
         if (it != editorIt->end()) {
             m_editorLineNumberFg = "#" + *it;
         }
+
+        it = editorIt->find("selected-word-background");
+        if (it != editorIt->end()) {
+            m_editorSelectedWordBg = "#" + *it;
+        }
+
+        it = editorIt->find("searched-word-background");
+        if (it != editorIt->end()) {
+            m_editorSearchedWordBg = "#" + *it;
+        }
+
+        it = editorIt->find("searched-word-cursor-background");
+        if (it != editorIt->end()) {
+            m_editorSearchedWordCursorBg = "#" + *it;
+        }
+
+        it = editorIt->find("incremental-searched-word-background");
+        if (it != editorIt->end()) {
+            m_editorIncrementalSearchedWordBg = "#" + *it;
+        }
     }
 }
 

+ 38 - 2
src/vconfigmanager.h

@@ -163,7 +163,11 @@ public:
     bool isCustomWebZoomFactor();
 
     const QString &getEditorCurrentLineBg() const;
-    QString getEditorTrailingSpaceBackground() const;
+    const QString &getEditorTrailingSpaceBg() const;
+    const QString &getEditorSelectedWordBg() const;
+    const QString &getEditorSearchedWordBg() const;
+    const QString &getEditorSearchedWordCursorBg() const;
+    const QString &getEditorIncrementalSearchedWordBg() const;
 
     const QString &getEditorVimNormalBg() const;
     const QString &getEditorVimInsertBg() const;
@@ -363,6 +367,18 @@ private:
     // Trailing space background color in editor.
     QString m_editorTrailingSpaceBg;
 
+    // Background color of selected word in editor.
+    QString m_editorSelectedWordBg;
+
+    // Background color of searched word in editor.
+    QString m_editorSearchedWordBg;
+
+    // Background color of searched word under cursor in editor.
+    QString m_editorSearchedWordCursorBg;
+
+    // Background color of incremental searched word in editor.
+    QString m_editorIncrementalSearchedWordBg;
+
     // Enable colde block syntax highlight.
     bool m_enableCodeBlockHighlight;
 
@@ -827,11 +843,31 @@ inline const QString &VConfigManager::getEditorCurrentLineBg() const
     return m_editorCurrentLineBg;
 }
 
-inline QString VConfigManager::getEditorTrailingSpaceBackground() const
+inline const QString &VConfigManager::getEditorTrailingSpaceBg() const
 {
     return m_editorTrailingSpaceBg;
 }
 
+inline const QString &VConfigManager::getEditorSelectedWordBg() const
+{
+    return m_editorSelectedWordBg;
+}
+
+inline const QString &VConfigManager::getEditorSearchedWordBg() const
+{
+    return m_editorSearchedWordBg;
+}
+
+inline const QString &VConfigManager::getEditorSearchedWordCursorBg() const
+{
+    return m_editorSearchedWordCursorBg;
+}
+
+inline const QString &VConfigManager::getEditorIncrementalSearchedWordBg() const
+{
+    return m_editorIncrementalSearchedWordBg;
+}
+
 inline const QString &VConfigManager::getEditorVimNormalBg() const
 {
     return m_editorVimNormalBg;

+ 9 - 0
src/vconstants.h

@@ -41,4 +41,13 @@ enum class TextDecoration { None,
                             Underline,
                             Strikethrough,
                             InlineCode };
+
+enum FindOption
+{
+    CaseSensitive = 0x1U,
+    WholeWordOnly = 0x2U,
+    RegularExpression = 0x4U,
+    IncrementalSearch = 0x8U
+};
+
 #endif

+ 31 - 21
src/vedit.cpp

@@ -6,8 +6,8 @@
 #include "vconfigmanager.h"
 #include "vtoc.h"
 #include "utils/vutils.h"
+#include "utils/veditutils.h"
 #include "veditoperations.h"
-#include "dialog/vfindreplacedialog.h"
 #include "vedittab.h"
 
 extern VConfigManager vconfig;
@@ -49,11 +49,11 @@ VEdit::VEdit(VFile *p_file, QWidget *p_parent)
     const int extraSelectionHighlightTimer = 500;
     const int labelSize = 64;
 
-    m_selectedWordColor = QColor("Yellow");
-    m_searchedWordColor = QColor(g_vnote->getColorFromPalette("Green4"));
-    m_searchedWordCursorColor = QColor("#64B5F6");
-    m_incrementalSearchedWordColor = QColor(g_vnote->getColorFromPalette("Purple2"));
-    m_trailingSpaceColor = QColor(vconfig.getEditorTrailingSpaceBackground());
+    m_selectedWordColor = QColor(vconfig.getEditorSelectedWordBg());
+    m_searchedWordColor = QColor(vconfig.getEditorSearchedWordBg());
+    m_searchedWordCursorColor = QColor(vconfig.getEditorSearchedWordCursorBg());
+    m_incrementalSearchedWordColor = QColor(vconfig.getEditorIncrementalSearchedWordBg());
+    m_trailingSpaceColor = QColor(vconfig.getEditorTrailingSpaceBg());
 
     QPixmap wrapPixmap(":/resources/icons/search_wrap.svg");
     m_wrapLabel = new QLabel(this);
@@ -151,11 +151,11 @@ void VEdit::scrollToLine(int p_lineNumber)
 {
     Q_ASSERT(p_lineNumber >= 0);
 
-    // Move the cursor to the end first
-    moveCursor(QTextCursor::End);
-    QTextCursor cursor(document()->findBlockByLineNumber(p_lineNumber));
-    cursor.movePosition(QTextCursor::EndOfBlock);
-    setTextCursor(cursor);
+    QTextBlock block = document()->findBlockByLineNumber(p_lineNumber);
+    if (block.isValid()) {
+        VEditUtils::scrollBlockInPage(this, block.blockNumber(), 0);
+        moveCursor(QTextCursor::EndOfBlock);
+    }
 }
 
 bool VEdit::isModified() const
@@ -178,7 +178,7 @@ void VEdit::insertImage()
     }
 }
 
-bool VEdit::peekText(const QString &p_text, uint p_options)
+bool VEdit::peekText(const QString &p_text, uint p_options, bool p_forward)
 {
     if (p_text.isEmpty()) {
         makeBlockVisible(document()->findBlock(textCursor().selectionStart()));
@@ -188,8 +188,10 @@ bool VEdit::peekText(const QString &p_text, uint p_options)
 
     bool wrapped = false;
     QTextCursor retCursor;
-    bool found = findTextHelper(p_text, p_options, true,
-                                textCursor().position() + 1, wrapped, retCursor);
+    bool found = findTextHelper(p_text, p_options, p_forward,
+                                p_forward ? textCursor().position() + 1
+                                          : textCursor().position(),
+                                wrapped, retCursor);
     if (found) {
         makeBlockVisible(document()->findBlock(retCursor.selectionStart()));
         highlightIncrementalSearchedWord(retCursor);
@@ -324,7 +326,8 @@ QList<QTextCursor> VEdit::findTextAll(const QString &p_text, uint p_options)
     return results;
 }
 
-bool VEdit::findText(const QString &p_text, uint p_options, bool p_forward)
+bool VEdit::findText(const QString &p_text, uint p_options, bool p_forward,
+                     QTextCursor *p_cursor, QTextCursor::MoveMode p_moveMode)
 {
     clearIncrementalSearchedWordHighlight();
 
@@ -333,12 +336,16 @@ bool VEdit::findText(const QString &p_text, uint p_options, bool p_forward)
         return false;
     }
 
+    QTextCursor cursor = textCursor();
     bool wrapped = false;
     QTextCursor retCursor;
     int matches = 0;
-    bool found = findTextHelper(p_text, p_options, p_forward,
-                                p_forward ? textCursor().position() + 1
-                                          : textCursor().position(),
+    int start = p_forward ? cursor.position() + 1 : cursor.position();
+    if (p_cursor) {
+        start = p_forward ? p_cursor->position() + 1 : p_cursor->position();
+    }
+
+    bool found = findTextHelper(p_text, p_options, p_forward, start,
                                 wrapped, retCursor);
     if (found) {
         Q_ASSERT(!retCursor.isNull());
@@ -346,9 +353,12 @@ bool VEdit::findText(const QString &p_text, uint p_options, bool p_forward)
             showWrapLabel();
         }
 
-        QTextCursor cursor = textCursor();
-        cursor.setPosition(retCursor.selectionStart());
-        setTextCursor(cursor);
+        if (p_cursor) {
+            p_cursor->setPosition(retCursor.selectionStart(), p_moveMode);
+        } else {
+            cursor.setPosition(retCursor.selectionStart(), p_moveMode);
+            setTextCursor(cursor);
+        }
 
         highlightSearchedWord(p_text, p_options);
         highlightSearchedWordUnderCursor(retCursor);

+ 7 - 2
src/vedit.h

@@ -83,9 +83,14 @@ public:
 
     // Used for incremental search.
     // User has enter the content to search, but does not enter the "find" button yet.
-    bool peekText(const QString &p_text, uint p_options);
+    bool peekText(const QString &p_text, uint p_options, bool p_forward = true);
+
+    // If @p_cursor is not now, set the position of @p_cursor instead of current
+    // cursor.
+    bool findText(const QString &p_text, uint p_options, bool p_forward,
+                  QTextCursor *p_cursor = NULL,
+                  QTextCursor::MoveMode p_moveMode = QTextCursor::MoveAnchor);
 
-    bool findText(const QString &p_text, uint p_options, bool p_forward);
     void replaceText(const QString &p_text, uint p_options,
                      const QString &p_replaceText, bool p_findNext);
     void replaceTextAll(const QString &p_text, uint p_options,

+ 1 - 1
src/vmainwindow.cpp

@@ -1773,10 +1773,10 @@ void VMainWindow::updateStatusInfo(const VEditTabInfo &p_info)
 
 void VMainWindow::handleVimStatusUpdated(const VVim *p_vim)
 {
+    m_vimIndicator->update(p_vim, m_curTab);
     if (!p_vim || !m_curTab || !m_curTab->isEditMode()) {
         m_vimIndicator->hide();
     } else {
-        m_vimIndicator->update(p_vim);
         m_vimIndicator->show();
     }
 }

+ 9 - 2
src/vmdedit.cpp

@@ -354,8 +354,15 @@ void VMdEdit::generateEditOutline()
 void VMdEdit::scrollToHeader(const VAnchor &p_anchor)
 {
     if (p_anchor.lineNumber == -1
-        || p_anchor.m_outlineIndex < 0
-        || p_anchor.m_outlineIndex >= m_headers.size()) {
+        || p_anchor.m_outlineIndex < 0) {
+        // Move to the start of document if m_headers is not empty.
+        // Otherwise, there is no outline, so just let it be.
+        if (!m_headers.isEmpty()) {
+            moveCursor(QTextCursor::Start);
+        }
+
+        return;
+    } else if (p_anchor.m_outlineIndex >= m_headers.size()) {
         return;
     }
 

+ 251 - 2
src/vvimindicator.cpp

@@ -10,6 +10,7 @@
 
 #include "vconfigmanager.h"
 #include "vbuttonwithwidget.h"
+#include "vedittab.h"
 
 extern VConfigManager vconfig;
 
@@ -21,6 +22,66 @@ VVimIndicator::VVimIndicator(QWidget *p_parent)
 
 void VVimIndicator::setupUI()
 {
+    m_cmdLineEdit = new VVimCmdLineEdit(this);
+    connect(m_cmdLineEdit, &VVimCmdLineEdit::commandCancelled,
+            this, [this](){
+                if (m_vim) {
+                    m_vim->processCommandLineCancelled();
+                }
+
+                if (m_editTab) {
+                    m_editTab->focusTab();
+                }
+
+                // NOTICE: m_cmdLineEdit should not hide itself before setting
+                // focus to edit tab.
+                m_cmdLineEdit->hide();
+            });
+
+    connect(m_cmdLineEdit, &VVimCmdLineEdit::commandFinished,
+            this, [this](VVim::CommandLineType p_type, const QString &p_cmd){
+                if (m_vim) {
+                    m_vim->processCommandLine(p_type, p_cmd);
+                }
+
+                if (m_editTab) {
+                    m_editTab->focusTab();
+                }
+
+                m_cmdLineEdit->hide();
+            });
+
+    connect(m_cmdLineEdit, &VVimCmdLineEdit::commandChanged,
+            this, [this](VVim::CommandLineType p_type, const QString &p_cmd){
+                if (m_vim) {
+                    m_vim->processCommandLineChanged(p_type, p_cmd);
+                }
+            });
+
+    connect(m_cmdLineEdit, &VVimCmdLineEdit::requestNextCommand,
+            this, [this](VVim::CommandLineType p_type, const QString &p_cmd){
+                if (m_vim) {
+                    QString cmd = m_vim->getNextCommandHistory(p_type, p_cmd);
+                    if (!cmd.isNull()) {
+                        m_cmdLineEdit->setCommand(cmd);
+                    } else {
+                        m_cmdLineEdit->restoreUserLastInput();
+                    }
+                }
+            });
+
+    connect(m_cmdLineEdit, &VVimCmdLineEdit::requestPreviousCommand,
+            this, [this](VVim::CommandLineType p_type, const QString &p_cmd){
+                if (m_vim) {
+                    QString cmd = m_vim->getPreviousCommandHistory(p_type, p_cmd);
+                    if (!cmd.isNull()) {
+                        m_cmdLineEdit->setCommand(cmd);
+                    }
+                }
+            });
+
+    m_cmdLineEdit->hide();
+
     m_modeLabel = new QLabel(this);
 
     m_regBtn = new VButtonWithWidget(QIcon(":/resources/icons/arrow_dropup.svg"),
@@ -60,6 +121,8 @@ void VVimIndicator::setupUI()
     m_keyLabel->setMinimumWidth(metric.width('A') * 5);
 
     QHBoxLayout *mainLayout = new QHBoxLayout(this);
+    mainLayout->addStretch();
+    mainLayout->addWidget(m_cmdLineEdit);
     mainLayout->addWidget(m_modeLabel);
     mainLayout->addWidget(m_regBtn);
     mainLayout->addWidget(m_markBtn);
@@ -154,9 +217,29 @@ static void fillTreeItemsWithRegisters(QTreeWidget *p_tree,
     p_tree->resizeColumnToContents(1);
 }
 
-void VVimIndicator::update(const VVim *p_vim)
+void VVimIndicator::update(const VVim *p_vim, const VEditTab *p_editTab)
 {
-    m_vim = p_vim;
+    m_editTab = const_cast<VEditTab *>(p_editTab);
+    if (m_vim != p_vim) {
+        // Disconnect from previous Vim.
+        if (m_vim) {
+            disconnect(m_vim.data(), 0, this, 0);
+        }
+
+        m_vim = const_cast<VVim *>(p_vim);
+        if (m_vim) {
+            // Connect signal.
+            connect(m_vim.data(), &VVim::commandLineTriggered,
+                    this, &VVimIndicator::triggerCommandLine);
+
+            m_cmdLineEdit->hide();
+        }
+    }
+
+    if (!m_vim) {
+        m_cmdLineEdit->hide();
+        return;
+    }
 
     VimMode mode = VimMode::Normal;
     QChar curRegName(' ');
@@ -230,3 +313,169 @@ void VVimIndicator::updateMarksTree(QWidget *p_widget)
     const QMap<QChar, VVim::Mark> &marks = m_vim->getMarks().getMarks();
     fillTreeItemsWithMarks(markTree, marks);
 }
+
+void VVimIndicator::triggerCommandLine(VVim::CommandLineType p_type)
+{
+    m_cmdLineEdit->reset(p_type);
+}
+
+VVimCmdLineEdit::VVimCmdLineEdit(QWidget *p_parent)
+    : QLineEdit(p_parent), m_type(VVim::CommandLineType::Invalid)
+{
+    // When user delete all the text, cancel command input.
+    connect(this, &VVimCmdLineEdit::textChanged,
+            this, [this](const QString &p_text){
+                if (p_text.isEmpty()) {
+                    emit commandCancelled();
+                } else {
+                    emit commandChanged(m_type, p_text.right(p_text.size() - 1));
+                }
+            });
+
+    connect(this, &VVimCmdLineEdit::textEdited,
+            this, [this](const QString &p_text){
+                if (p_text.size() < 2) {
+                    m_userLastInput.clear();
+                } else {
+                    m_userLastInput = p_text.right(p_text.size() - 1);
+                }
+            });
+}
+
+QString VVimCmdLineEdit::getCommand() const
+{
+    QString tx = text();
+    if (tx.size() < 2) {
+        return "";
+    } else {
+        return tx.right(tx.size() - 1);
+    }
+}
+
+QString VVimCmdLineEdit::commandLineTypeLeader(VVim::CommandLineType p_type)
+{
+    QString leader;
+    switch (p_type) {
+    case VVim::CommandLineType::Command:
+        leader = ":";
+        break;
+
+    case VVim::CommandLineType::SearchForward:
+        leader = "/";
+        break;
+
+    case VVim::CommandLineType::SearchBackward:
+        leader = "?";
+        break;
+
+    case VVim::CommandLineType::Invalid:
+        leader.clear();
+        break;
+
+    default:
+        Q_ASSERT(false);
+        break;
+    }
+
+    return leader;
+}
+
+void VVimCmdLineEdit::reset(VVim::CommandLineType p_type)
+{
+    m_type = p_type;
+    m_userLastInput.clear();
+    setCommand("");
+    show();
+    setFocus();
+}
+
+// See if @p_modifiers is Control which is different on macOs and Windows.
+static bool isControlModifier(int p_modifiers)
+{
+#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
+    return p_modifiers == Qt::MetaModifier;
+#else
+    return p_modifiers == Qt::ControlModifier;
+#endif
+}
+
+void VVimCmdLineEdit::keyPressEvent(QKeyEvent *p_event)
+{
+    int key = p_event->key();
+    int modifiers = p_event->modifiers();
+
+    if ((key == Qt::Key_Return && modifiers == Qt::NoModifier)
+        || (key == Qt::Key_Enter && modifiers == Qt::KeypadModifier)) {
+        // Enter, complete the command line input.
+        p_event->accept();
+        emit commandFinished(m_type, getCommand());
+        return;
+    } else if (key == Qt::Key_Escape
+               || (key == Qt::Key_BracketLeft && isControlModifier(modifiers))) {
+        // Exit command line input.
+        setText(commandLineTypeLeader(m_type));
+        p_event->accept();
+        emit commandCancelled();
+        return;
+    }
+
+    switch (key) {
+    case Qt::Key_U:
+        if (isControlModifier(modifiers)) {
+            // Ctrl+U, delete all user input.
+            setText(commandLineTypeLeader(m_type));
+            p_event->accept();
+            return;
+        }
+
+        break;
+
+    case Qt::Key_N:
+        if (!isControlModifier(modifiers)) {
+            break;
+        }
+        // Ctrl+N, request next command.
+        // Fall through.
+    case Qt::Key_Down:
+    {
+        emit requestNextCommand(m_type, getCommand());
+        p_event->accept();
+        return;
+    }
+
+    case Qt::Key_P:
+        if (!isControlModifier(modifiers)) {
+            break;
+        }
+        // Ctrl+P, request previous command.
+        // Fall through.
+    case Qt::Key_Up:
+    {
+        emit requestPreviousCommand(m_type, getCommand());
+        p_event->accept();
+        return;
+    }
+
+    default:
+        break;
+    }
+
+    QLineEdit::keyPressEvent(p_event);
+}
+
+void VVimCmdLineEdit::focusOutEvent(QFocusEvent *p_event)
+{
+    if (p_event->reason() != Qt::ActiveWindowFocusReason) {
+        emit commandCancelled();
+    }
+}
+
+void VVimCmdLineEdit::setCommand(const QString &p_cmd)
+{
+    setText(commandLineTypeLeader(m_type) + p_cmd);
+}
+
+void VVimCmdLineEdit::restoreUserLastInput()
+{
+    setCommand(m_userLastInput);
+}

+ 62 - 2
src/vvimindicator.h

@@ -2,10 +2,63 @@
 #define VVIMINDICATOR_H
 
 #include <QWidget>
+#include <QLineEdit>
+#include <QPointer>
 #include "utils/vvim.h"
 
 class QLabel;
 class VButtonWithWidget;
+class QKeyEvent;
+class QFocusEvent;
+class VEditTab;
+
+class VVimCmdLineEdit : public QLineEdit
+{
+    Q_OBJECT
+
+public:
+    explicit VVimCmdLineEdit(QWidget *p_parent = 0);
+
+    void reset(VVim::CommandLineType p_type);
+
+    // Set the command to @p_cmd with leader unchanged.
+    void setCommand(const QString &p_cmd);
+
+    // Get the command.
+    QString getCommand() const;
+
+    void restoreUserLastInput();
+
+signals:
+    // User has finished the input and the command is ready to execute.
+    void commandFinished(VVim::CommandLineType p_type, const QString &p_cmd);
+
+    // User cancelled the input.
+    void commandCancelled();
+
+    // User request the next command in the history.
+    void requestNextCommand(VVim::CommandLineType p_type, const QString &p_cmd);
+
+    // User request the previous command in the history.
+    void requestPreviousCommand(VVim::CommandLineType p_type, const QString &p_cmd);
+
+    // Emit when the input text changed.
+    void commandChanged(VVim::CommandLineType p_type, const QString &p_cmd);
+
+protected:
+    void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE;
+
+    void focusOutEvent(QFocusEvent *p_event) Q_DECL_OVERRIDE;
+
+private:
+    // Return the leader of @p_type.
+    QString commandLineTypeLeader(VVim::CommandLineType p_type);
+
+    VVim::CommandLineType m_type;
+
+    // The latest command user input.
+    QString m_userLastInput;
+};
 
 class VVimIndicator : public QWidget
 {
@@ -15,18 +68,24 @@ public:
     explicit VVimIndicator(QWidget *p_parent = 0);
 
     // Update indicator according to @p_vim.
-    void update(const VVim *p_vim);
+    void update(const VVim *p_vim, const VEditTab *p_editTab);
 
 private slots:
     void updateRegistersTree(QWidget *p_widget);
 
     void updateMarksTree(QWidget *p_widget);
 
+    // Vim request to trigger command line.
+    void triggerCommandLine(VVim::CommandLineType p_type);
+
 private:
     void setupUI();
 
     QString modeToString(VimMode p_mode) const;
 
+    // Command line input.
+    VVimCmdLineEdit *m_cmdLineEdit;
+
     // Indicate the mode.
     QLabel *m_modeLabel;
 
@@ -39,7 +98,8 @@ private:
     // Indicate the pending keys.
     QLabel *m_keyLabel;
 
-    const VVim *m_vim;
+    QPointer<VVim> m_vim;
+    QPointer<VEditTab> m_editTab;
 };
 
 #endif // VVIMINDICATOR_H