瀏覽代碼

vim-mode: support word-related movement

- `w`, `W`, `e`, `E`, `b`, `B`, `ge`, and `gE`.
Le Tan 8 年之前
父節點
當前提交
cbf207d9ed
共有 2 個文件被更改,包括 370 次插入4 次删除
  1. 362 0
      src/utils/vvim.cpp
  2. 8 4
      src/utils/vvim.h

+ 362 - 0
src/utils/vvim.cpp

@@ -49,6 +49,113 @@ static void moveCursorFirstNonSpaceCharacter(QTextCursor &p_cursor,
     p_cursor.setPosition(block.position() + idx, p_mode);
 }
 
+// Find the start and end of the WORD @p_cursor locates in (within a single block).
+// @p_start and @p_end will be the global position of the start and end of the WORD.
+// @p_start will equals to @p_end if @p_cursor is a space.
+static void findCurrentWORD(const QTextCursor &p_cursor, int &p_start, int &p_end)
+{
+    QTextBlock block = p_cursor.block();
+    QString text = block.text();
+    int pib = p_cursor.positionInBlock();
+
+    // Find the start.
+    p_start = p_end = -1;
+    for (int i = pib - 1; i >= 0; --i) {
+        if (text[i].isSpace()) {
+            ++i;
+            p_start = i;
+            break;
+        }
+    }
+
+    if (p_start == -1) {
+        p_start = 0;
+    }
+
+    // Find the end.
+    for (int i = pib; i < text.size(); ++i) {
+        if (text[i].isSpace()) {
+            p_end = i;
+            break;
+        }
+    }
+
+    if (p_end == -1) {
+        p_end = block.length() - 1;
+    }
+
+    p_start += block.position();
+    p_end += block.position();
+}
+
+// Move @p_cursor to skip spaces if current cursor is placed at a space
+// (may move across blocks). It will stop by the empty block on the way.
+// Forward: wwwwsssss|wwww
+// Backward: wwww|ssssswwww
+static void moveCursorAcrossSpaces(QTextCursor &p_cursor,
+                                   QTextCursor::MoveMode p_mode,
+                                   bool p_forward)
+{
+    while (true) {
+        QTextBlock block = p_cursor.block();
+        QString text = block.text();
+        int pib = p_cursor.positionInBlock();
+
+        if (p_forward) {
+            for (; pib < text.size(); ++pib) {
+                if (!text[pib].isSpace()) {
+                    break;
+                }
+            }
+
+            if (pib == text.size()) {
+                // Move to next block.
+                p_cursor.movePosition(QTextCursor::Down, p_mode, 1);
+                if (block.blockNumber() == p_cursor.block().blockNumber()) {
+                    // Already at the last block.
+                    p_cursor.movePosition(QTextCursor::EndOfBlock, p_mode, 1);
+                    break;
+                } else {
+                    p_cursor.movePosition(QTextCursor::StartOfBlock, p_mode, 1);
+                    if (p_cursor.block().length() <= 1) {
+                        break;
+                    }
+                }
+            } else {
+                // Found non-space character.
+                p_cursor.setPosition(block.position() + pib, p_mode);
+                break;
+            }
+        } else {
+            int idx = pib - 1;
+            for (; idx >= 0; --idx) {
+                if (!text[idx].isSpace()) {
+                    break;
+                }
+            }
+
+            if (idx == -1) {
+                // Move to previous block.
+                p_cursor.movePosition(QTextCursor::Up, p_mode, 1);
+                if (block.blockNumber() == p_cursor.block().blockNumber()) {
+                    // Already at the first block.
+                    p_cursor.movePosition(QTextCursor::StartOfBlock, p_mode, 1);
+                    break;
+                } else {
+                    p_cursor.movePosition(QTextCursor::EndOfBlock, p_mode, 1);
+                    if (p_cursor.block().length() <= 1) {
+                        break;
+                    }
+                }
+            } else {
+                // Found non-space character.
+                p_cursor.setPosition(block.position() + idx + 1, p_mode);
+                break;
+            }
+        }
+    }
+}
+
 bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
 {
     bool ret = false;
@@ -389,6 +496,28 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
         if (modifiers == Qt::ControlModifier) {
             // Ctrl+B, page up, fall through.
             modifiers = Qt::NoModifier;
+        } else if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) {
+            tryGetRepeatToken(m_keys, m_tokens);
+            if (!m_keys.isEmpty()) {
+                // Not a valid sequence.
+                break;
+            }
+
+            // b, go to the start of previous or current word.
+            Movement mm = Movement::WordBackward;
+            if (modifiers == Qt::ShiftModifier) {
+                // B, go to the start of previous or current WORD.
+                mm = Movement::WORDBackward;
+            }
+
+            if (m_tokens.isEmpty()) {
+                // Move.
+                m_tokens.append(Token(Action::Move));
+            }
+
+            m_tokens.append(Token(mm));
+            processCommand(m_tokens);
+            break;
         } else {
             break;
         }
@@ -563,6 +692,73 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
         break;
     }
 
+    case Qt::Key_W:
+    {
+        if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) {
+            tryGetRepeatToken(m_keys, m_tokens);
+            if (!m_keys.isEmpty()) {
+                // Not a valid sequence.
+                break;
+            }
+
+            // w, go to the start of next word.
+            Movement mm = Movement::WordForward;
+            if (modifiers == Qt::ShiftModifier) {
+                // W, go to the start of next WORD.
+                mm = Movement::WORDForward;
+            }
+
+            if (m_tokens.isEmpty()) {
+                // Move.
+                m_tokens.append(Token(Action::Move));
+            }
+
+            m_tokens.append(Token(mm));
+            processCommand(m_tokens);
+        }
+
+        break;
+    }
+
+    case Qt::Key_E:
+    {
+        // e, E, ge, gE.
+        if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) {
+            tryGetRepeatToken(m_keys, m_tokens);
+            Movement mm = Movement::Invalid;
+            if (!m_keys.isEmpty()) {
+                if (m_keys.size() == 1 && m_keys.at(0) == Key(Qt::Key_G)) {
+                    // ge, gE.
+                    if (modifiers == Qt::NoModifier) {
+                        mm = Movement::BackwardEndOfWord;
+                    } else {
+                        mm = Movement::BackwardEndOfWORD;
+                    }
+                } else {
+                    // Not a valid sequence.
+                    break;
+                }
+            } else {
+                // e, E.
+                if (modifiers == Qt::NoModifier) {
+                    mm = Movement::ForwardEndOfWord;
+                } else {
+                    mm = Movement::ForwardEndOfWORD;
+                }
+            }
+
+            if (m_tokens.isEmpty()) {
+                // Move.
+                m_tokens.append(Token(Action::Move));
+            }
+
+            m_tokens.append(Token(mm));
+            processCommand(m_tokens);
+        }
+
+        break;
+    }
+
     default:
         break;
     }
@@ -947,6 +1143,172 @@ void VVim::processMoveAction(QList<Token> &p_tokens)
         break;
     }
 
+    case Movement::WordForward:
+    {
+        // Go to the start of next word.
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        cursor.movePosition(QTextCursor::NextWord, moveMode, repeat);
+        hasMoved = true;
+        break;
+    }
+
+    case Movement::WORDForward:
+    {
+        // Go to the start of next WORD.
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        for (int i = 0; i < repeat; ++i) {
+            int start, end;
+            // [start, end] is current WORD.
+            findCurrentWORD(cursor, start, end);
+
+            // Move cursor to end of current WORD.
+            cursor.setPosition(end, moveMode);
+
+            // Skip spaces.
+            moveCursorAcrossSpaces(cursor, moveMode, true);
+        }
+
+        hasMoved = true;
+        break;
+    }
+
+    case Movement::ForwardEndOfWord:
+    {
+        // Go to the end of current word or next word.
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        int pos = cursor.position();
+        // First move to the end of current word.
+        cursor.movePosition(QTextCursor::EndOfWord, moveMode, 1);
+        if (pos != cursor.position()) {
+            // We did move.
+            repeat -= 1;
+        }
+
+        if (repeat) {
+            cursor.movePosition(QTextCursor::NextWord, moveMode, repeat);
+            cursor.movePosition(QTextCursor::EndOfWord, moveMode);
+        }
+
+        hasMoved = true;
+        break;
+    }
+
+    case Movement::ForwardEndOfWORD:
+    {
+        // Go to the end of current WORD or next WORD.
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        for (int i = 0; i < repeat; ++i) {
+            // Skip spaces.
+            moveCursorAcrossSpaces(cursor, moveMode, true);
+
+            int start, end;
+            // [start, end] is current WORD.
+            findCurrentWORD(cursor, start, end);
+
+            // Move cursor to the end of current WORD.
+            cursor.setPosition(end, moveMode);
+        }
+
+        hasMoved = true;
+        break;
+    }
+
+    case Movement::WordBackward:
+    {
+        // Go to the start of previous word or current word.
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        int pos = cursor.position();
+        // first move to the start of current word.
+        cursor.movePosition(QTextCursor::StartOfWord, moveMode, 1);
+        if (pos != cursor.position()) {
+            // We did move.
+            repeat -= 1;
+        }
+
+        if (repeat) {
+            cursor.movePosition(QTextCursor::PreviousWord, moveMode, repeat);
+        }
+
+        hasMoved = true;
+        break;
+    }
+
+    case Movement::WORDBackward:
+    {
+        // Go to the start of previous WORD or current WORD.
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        for (int i = 0; i < repeat; ++i) {
+            // Skip Spaces.
+            moveCursorAcrossSpaces(cursor, moveMode, false);
+
+            int start, end;
+            // [start, end] is current WORD.
+            findCurrentWORD(cursor, start, end);
+
+            // Move cursor to the start of current WORD.
+            cursor.setPosition(start, moveMode);
+        }
+
+        hasMoved = true;
+        break;
+    }
+
+    case Movement::BackwardEndOfWord:
+    {
+        // Go to the end of previous word.
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        int pib = cursor.positionInBlock();
+        if (!(pib > 0 && cursor.block().text()[pib -1].isSpace())) {
+            ++repeat;
+        }
+
+        cursor.movePosition(QTextCursor::PreviousWord, moveMode, repeat);
+        cursor.movePosition(QTextCursor::EndOfWord, moveMode, 1);
+        hasMoved = true;
+        break;
+    }
+
+    case Movement::BackwardEndOfWORD:
+    {
+        // Go to the end of previous WORD.
+        if (repeat == -1) {
+            repeat = 1;
+        }
+
+        for (int i = 0; i < repeat; ++i) {
+            int start, end;
+            findCurrentWORD(cursor, start, end);
+
+            cursor.setPosition(start, moveMode);
+
+            moveCursorAcrossSpaces(cursor, moveMode, false);
+        }
+
+        hasMoved = true;
+        break;
+    }
+
     default:
         break;
     }

+ 8 - 4
src/utils/vvim.h

@@ -88,10 +88,6 @@ private:
         UnIndent,
         ToUpper,
         ToLower,
-        DeleteToClipboard,
-        CopyToClipboard,
-        PasteFromClipboard,
-        ChangeToClipboard,
         Invalid
     };
 
@@ -114,6 +110,14 @@ private:
         LineJump,
         StartOfDocument,
         EndOfDocument,
+        WordForward,
+        WORDForward,
+        ForwardEndOfWord,
+        ForwardEndOfWORD,
+        WordBackward,
+        WORDBackward,
+        BackwardEndOfWord,
+        BackwardEndOfWORD,
         Invalid
     };