From e1acd6e9a2607f10331afa655d619d406bced226 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Fri, 30 Jun 2017 19:17:42 +0800 Subject: [PATCH] vim-mode: support r for replacement --- src/resources/docs/shortcuts_en.md | 8 ++- src/resources/docs/shortcuts_zh.md | 7 +- src/utils/vvim.cpp | 105 +++++++++++++++++++++++++++++ src/utils/vvim.h | 32 +++++++-- 4 files changed, 143 insertions(+), 9 deletions(-) diff --git a/src/resources/docs/shortcuts_en.md b/src/resources/docs/shortcuts_en.md index 6eb7bb19..83fc9e12 100644 --- a/src/resources/docs/shortcuts_en.md +++ b/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 `y/d/p` equals to `"+y/d/p`, which will access the system's clipboard. + - Currently `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. diff --git a/src/resources/docs/shortcuts_zh.md b/src/resources/docs/shortcuts_zh.md index ec4dac83..bc15ccb7 100644 --- a/src/resources/docs/shortcuts_zh.md +++ b/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`) - - 目前 `y/d/p` 等同于 `"+y/d/p`, 从而可以访问系统剪切板。 + - 目前 `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的宏和重复(`.`)特性。 diff --git a/src/utils/vvim.cpp b/src/utils/vvim.cpp index 23c60b2c..bcd84c1f 100644 --- a/src/utils/vvim.cpp +++ b/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 &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 &p_tokens, bool p_next) } } +void VVim::processReplaceAction(QList &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()) { diff --git a/src/utils/vvim.h b/src/utils/vvim.h index aa3cfd0a..0e0c9c90 100644 --- a/src/utils/vvim.h +++ b/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 &p_tokens, bool p_next); + // Action::Replace. + void processReplaceAction(QList &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);