Explorar o código

vim-mode: support r for replacement

Le Tan %!s(int64=8) %!d(string=hai) anos
pai
achega
e1acd6e9a2
Modificáronse 4 ficheiros con 143 adicións e 9 borrados
  1. 6 2
      src/resources/docs/shortcuts_en.md
  2. 5 2
      src/resources/docs/shortcuts_zh.md
  3. 105 0
      src/utils/vvim.cpp
  4. 27 5
      src/utils/vvim.h

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

@@ -120,14 +120,18 @@ VNote supports a simple but useful Vim mode, including **Normal**, **Insert**, *
 
 VNote supports following features of Vim:
 
-- `d`, `c`, `y`, `p`, `<`, `>`, `gu`, `gU` actions;
+- `r`, `s`, `i`, `I`, `a`, `A`, `o`, and `O`;
+- Actions `d`, `c`, `y`, `p`, `<`, `>`, `gu`, and `gU`;
+- Movements `h/j/k/l`, `gj/gk`, `Ctrl+U`, `Ctrl+D`, `gg`, `G`, `0`, `^`, and `$`;
 - Marks `a-z`;
 - Registers `"`, `_`, `+`, `a-z`(`A-Z`);
 - 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.
+    - Currently `<leader>y/d/p` equals to `"+y/d/p`, which will access the system's clipboard;
 - `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!`;
 
 For now, VNote does **NOT** support the macro and repeat(`.`) features of Vim.
 

+ 5 - 2
src/resources/docs/shortcuts_zh.md

@@ -120,14 +120,17 @@ VNote支持一个简单但有用的Vim模式,包括 **正常**, **插入**
 
 VNote支持以下几个Vim的特性:
 
-- `d`, `c`, `y`, `p`, `<`, `>`, `gu`, `gU` 操作;
+- `r`, `s`, `i`, `I`, `a`, `A`, `o`, `O`;
+- 操作 `d`, `c`, `y`, `p`, `<`, `>`, `gu`, `gU`;
 - 标记 `a-z`;
 - 寄存器 `"`, `_`, `+`, `a-z`(`A-Z`);
 - 跳转位置列表 (`Ctrl+O` and `Ctrl+I`);
 - 前导键 (`Space`)
-    - 目前 `<leader>y/d/p` 等同于 `"+y/d/p`, 从而可以访问系统剪切板
+    - 目前 `<leader>y/d/p` 等同于 `"+y/d/p`, 从而可以访问系统剪切板
 - `zz`, `zb`, `zt`;
 - `u` 和 `Ctrl+R` 撤销和重做;
+- 文本对象 `i/a`:word, WORD, `''`, `""`, `` ` ` ``, `()`, `[]`, `<>`, and `{}`;
+- 命令行 `:w`, `:wq`, `:x`, `:q`, and `:q!`;
 
 VNote目前暂时不支持Vim的宏和重复(`.`)特性。
 

+ 105 - 0
src/utils/vvim.cpp

@@ -351,6 +351,31 @@ static bool isControlModifier(int p_modifiers)
 #endif
 }
 
+// Replace each of the character of selected text with @p_char.
+// Returns true if replacement has taken place.
+// Need to setTextCursor() after calling this.
+static bool replaceSelectedTextWithCharacter(QTextCursor &p_cursor, QChar p_char)
+{
+    if (!p_cursor.hasSelection()) {
+        return false;
+    }
+
+    int start = p_cursor.selectionStart();
+    int end = p_cursor.selectionEnd();
+    p_cursor.setPosition(start, QTextCursor::MoveAnchor);
+    while (p_cursor.position() < end) {
+        if (p_cursor.atBlockEnd()) {
+            p_cursor.movePosition(QTextCursor::NextCharacter);
+        } else {
+            p_cursor.deleteChar();
+            // insertText() will move the cursor right after the inserted text.
+            p_cursor.insertText(p_char);
+        }
+    }
+
+    return true;
+}
+
 bool VVim::handleKeyPressEvent(QKeyEvent *p_event, int *p_autoIndentPos)
 {
     bool ret = handleKeyPressEvent(p_event->key(), p_event->modifiers(), p_autoIndentPos);
@@ -511,6 +536,15 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
         goto clear_accept;
     }
 
+    if (expectingReplaceCharacter()) {
+        // Expecting a character to replace with for r.
+        addActionToken(Action::Replace);
+        addKeyToken(keyInfo);
+        processCommand(m_tokens);
+
+        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.
@@ -1632,6 +1666,13 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
             addActionToken(Action::Redo);
             processCommand(m_tokens);
             break;
+        } else if (modifiers == Qt::NoModifier) {
+            // r, replace.
+            tryGetRepeatToken(m_keys, m_tokens);
+            if (m_keys.isEmpty() && !hasActionToken()) {
+                m_keys.append(keyInfo);
+                goto accept;
+            }
         }
 
         break;
@@ -1974,6 +2015,10 @@ void VVim::processCommand(QList<Token> &p_tokens)
         processJumpLocationAction(p_tokens, true);
         break;
 
+    case Action::Replace:
+        processReplaceAction(p_tokens);
+        break;
+
     default:
         p_tokens.clear();
         break;
@@ -3971,6 +4016,55 @@ void VVim::processJumpLocationAction(QList<Token> &p_tokens, bool p_next)
     }
 }
 
+void VVim::processReplaceAction(QList<Token> &p_tokens)
+{
+    int repeat = 1;
+    QChar replaceChar;
+    Q_ASSERT(!p_tokens.isEmpty());
+    Token to = p_tokens.takeFirst();
+    if (to.isRepeat()) {
+        repeat = to.m_repeat;
+        Q_ASSERT(!p_tokens.isEmpty());
+        to = p_tokens.takeFirst();
+    }
+
+    Q_ASSERT(to.isKey() && p_tokens.isEmpty());
+    replaceChar = keyToChar(to.m_key.m_key, to.m_key.m_modifiers);
+    if (replaceChar.isNull()) {
+        return;
+    }
+
+    if (!(checkMode(VimMode::Normal)
+          || checkMode(VimMode::Visual)
+          || checkMode(VimMode::VisualLine))) {
+        return;
+    }
+
+    // Replace the next repeat characters with replaceChar until the end of line.
+    // If repeat is greater than the number of left characters in current line,
+    // do nothing.
+    // In visual mode, repeat is ignored.
+    QTextCursor cursor = m_editor->textCursor();
+    cursor.beginEditBlock();
+    if (checkMode(VimMode::Normal)) {
+        // Select the characters to be replaced.
+        cursor.clearSelection();
+        int pib = cursor.positionInBlock();
+        int nrChar = cursor.block().length() - 1 - pib;
+        if (repeat <= nrChar) {
+            cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, repeat);
+        }
+    }
+
+    bool changed = replaceSelectedTextWithCharacter(cursor, replaceChar);
+    cursor.endEditBlock();
+
+    if (changed) {
+        m_editor->setTextCursor(cursor);
+        setMode(VimMode::Normal);
+    }
+}
+
 bool VVim::clearSelection()
 {
     QTextCursor cursor = m_editor->textCursor();
@@ -4051,6 +4145,12 @@ bool VVim::expectingCharacterTarget() const
             || key == Key(Qt::Key_T, Qt::ShiftModifier));
 }
 
+bool VVim::expectingReplaceCharacter() const
+{
+    return m_keys.size() == 1
+           && m_keys.first() == Key(Qt::Key_R, Qt::NoModifier);
+}
+
 bool VVim::expectingCommandLineInput() const
 {
     return m_cmdMode;
@@ -4227,6 +4327,11 @@ void VVim::addMovementToken(Movement p_movement, Key p_key)
     m_tokens.append(Token(p_movement, p_key));
 }
 
+void VVim::addKeyToken(Key p_key)
+{
+    m_tokens.append(Token(p_key));
+}
+
 void VVim::deleteSelectedText(QTextCursor &p_cursor, bool p_clearEmptyBlock)
 {
     if (p_cursor.hasSelection()) {

+ 27 - 5
src/utils/vvim.h

@@ -262,6 +262,7 @@ private:
         RedrawAtBottom,
         JumpPreviousLocation,
         JumpNextLocation,
+        Replace,
         Invalid
     };
 
@@ -326,7 +327,7 @@ private:
         Invalid
     };
 
-    enum class TokenType { Action = 0, Repeat, Movement, Range, Invalid };
+    enum class TokenType { Action = 0, Repeat, Movement, Range, Key, Invalid };
 
     struct Token
     {
@@ -345,6 +346,9 @@ private:
         Token(Range p_range)
             : m_type(TokenType::Range), m_range(p_range) {}
 
+        Token(Key p_key)
+            : m_type(TokenType::Key), m_key(p_key) {}
+
         Token() : m_type(TokenType::Invalid) {}
 
         bool isRepeat() const
@@ -367,6 +371,11 @@ private:
             return m_type == TokenType::Range;
         }
 
+        bool isKey() const
+        {
+            return m_type == TokenType::Key;
+        }
+
         bool isValid() const
         {
             return m_type != TokenType::Invalid;
@@ -392,6 +401,10 @@ private:
                 str = QString("range %1").arg((int)m_range);
                 break;
 
+            case TokenType::Key:
+                str = QString("key %1 %2").arg(m_key.m_key).arg(m_key.m_modifiers);
+                break;
+
             default:
                 str = "invalid";
             }
@@ -409,7 +422,7 @@ private:
             Movement m_movement;
         };
 
-        // Used in some Movement.
+        // Used in some Movement and Key Token.
         Key m_key;
     };
 
@@ -503,6 +516,9 @@ private:
     // Action::JumpPreviousLocation and Action::JumpNextLocation action.
     void processJumpLocationAction(QList<Token> &p_tokens, bool p_next);
 
+    // Action::Replace.
+    void processReplaceAction(QList<Token> &p_tokens);
+
     // Clear selection if there is any.
     // Returns true if there is selection.
     bool clearSelection();
@@ -525,6 +541,9 @@ private:
     // Check m_keys to see if we are expecting a target for f/t/F/T command.
     bool expectingCharacterTarget() const;
 
+    // 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;
 
@@ -560,15 +579,18 @@ private:
     // Get the repeat token from m_tokens.
     Token *getRepeatToken();
 
-    // Add an Range token at the end of m_tokens.
+    // Add a Range token at the end of m_tokens.
     void addRangeToken(Range p_range);
 
-    // Add an Movement token at the end of m_tokens.
+    // Add a Movement token at the end of m_tokens.
     void addMovementToken(Movement p_movement);
 
-    // Add an Movement token at the end of m_tokens.
+    // Add a Movement token at the end of m_tokens.
     void addMovementToken(Movement p_movement, Key p_key);
 
+    // Add a Key token at the end of m_tokens.
+    void addKeyToken(Key p_key);
+
     // Delete selected text if there is any.
     // @p_clearEmptyBlock: whether to remove the empty block after deletion.
     void deleteSelectedText(QTextCursor &p_cursor, bool p_clearEmptyBlock);