From 7871965bf884a01089e84d5d3cb0fc31d1dff269 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Sat, 17 Jun 2017 13:57:03 +0800 Subject: [PATCH] vim-mode: support f/F/t/T movement Support `;` and `,` to repeat last find movement. --- src/utils/veditutils.cpp | 41 ++++++ src/utils/veditutils.h | 8 ++ src/utils/vvim.cpp | 271 +++++++++++++++++++++++++++++++++++++-- src/utils/vvim.h | 40 +++++- 4 files changed, 348 insertions(+), 12 deletions(-) diff --git a/src/utils/veditutils.cpp b/src/utils/veditutils.cpp index 8cd360d4..663d2f12 100644 --- a/src/utils/veditutils.cpp +++ b/src/utils/veditutils.cpp @@ -210,3 +210,44 @@ void VEditUtils::unindentBlock(QTextCursor &p_cursor, } } } + +bool VEditUtils::findTargetWithinBlock(QTextCursor &p_cursor, + QTextCursor::MoveMode p_mode, + QChar p_target, + bool p_forward, + bool p_inclusive) +{ + qDebug() << "find target" << p_target << p_forward << p_inclusive; + QTextBlock block = p_cursor.block(); + QString text = block.text(); + int pib = p_cursor.positionInBlock(); + int delta = p_forward ? 1 : -1; + int idx = pib + delta; + +repeat: + for (; idx < text.size() && idx >= 0; idx += delta) { + if (text[idx] == p_target) { + break; + } + } + + if (idx < 0 || idx >= text.size()) { + return false; + } + + if ((p_forward && p_inclusive && p_mode == QTextCursor::KeepAnchor) + || (!p_forward && !p_inclusive)) { + ++idx; + } else if (p_forward && !p_inclusive && p_mode == QTextCursor::MoveAnchor) { + --idx; + } + + if (idx == pib) { + // We need to skip current match. + idx = pib + delta * 2; + goto repeat; + } + + p_cursor.setPosition(block.position() + idx, p_mode); + return true; +} diff --git a/src/utils/veditutils.h b/src/utils/veditutils.h index 71b65c47..209d0b3e 100644 --- a/src/utils/veditutils.h +++ b/src/utils/veditutils.h @@ -63,6 +63,14 @@ public: static void unindentBlock(QTextCursor &p_cursor, const QString &p_indentationText); + // Find a char within a block. + // Returns true if target is found. + static bool findTargetWithinBlock(QTextCursor &p_cursor, + QTextCursor::MoveMode p_mode, + QChar p_target, + bool p_forward, + bool p_inclusive); + private: VEditUtils() {} }; diff --git a/src/utils/vvim.cpp b/src/utils/vvim.cpp index 840ccbdf..6c63c997 100644 --- a/src/utils/vvim.cpp +++ b/src/utils/vvim.cpp @@ -308,6 +308,34 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event) goto clear_accept; } + if (expectingCharacterTarget()) { + // Expecting a target character for f/F/t/T. + Movement mm = Movement::Invalid; + const Key &aKey = m_keys.first(); + if (aKey.m_key == Qt::Key_F) { + if (aKey.m_modifiers == Qt::NoModifier) { + mm = Movement::FindForward; + } else { + mm = Movement::FindBackward; + } + } else { + if (aKey.m_modifiers == Qt::NoModifier) { + mm = Movement::TillForward; + } else { + mm = Movement::TillBackward; + } + } + + V_ASSERT(mm != Movement::Invalid); + + tryAddMoveAction(); + addMovementToken(mm, keyInfo); + m_lastFindToken = m_tokens.last(); + processCommand(m_tokens); + + goto clear_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) { @@ -1164,6 +1192,82 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event) break; } + case Qt::Key_F: + { + if (m_mode == VimMode::VisualLine) { + break; + } + + if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) { + // f/F, find forward/backward within a block. + tryGetRepeatToken(m_keys, m_tokens); + + if (m_keys.isEmpty()) { + m_keys.append(keyInfo); + goto accept; + } + + break; + } + + break; + } + + case Qt::Key_T: + { + if (m_mode == VimMode::VisualLine) { + break; + } + + if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) { + // t/T, find till forward/backward within a block. + tryGetRepeatToken(m_keys, m_tokens); + + if (m_keys.isEmpty()) { + m_keys.append(keyInfo); + goto accept; + } + + break; + } + + break; + } + + case Qt::Key_Comma: + { + if (m_mode == VimMode::VisualLine) { + break; + } + + // ,, repeat last find target movement, but reversely. + tryGetRepeatToken(m_keys, m_tokens); + + if (m_keys.isEmpty()) { + repeatLastFindMovement(true); + break; + } + + break; + } + + case Qt::Key_Semicolon: + { + if (m_mode == VimMode::VisualLine) { + break; + } + + // ;, repeat last find target movement. + tryGetRepeatToken(m_keys, m_tokens); + + if (m_keys.isEmpty()) { + repeatLastFindMovement(false); + break; + } + + break; + } + default: break; } @@ -1327,7 +1431,7 @@ void VVim::processMoveAction(QList &p_tokens) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor; bool hasMoved = processMovement(cursor, m_editor->document(), - moveMode, mvToken.m_movement, repeat); + moveMode, mvToken, repeat); if (hasMoved) { // Maintain positionInBlock. @@ -1358,13 +1462,80 @@ void VVim::processMoveAction(QList &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, - Movement p_movement, int p_repeat) + const Token &p_token, int p_repeat) { - bool hasMoved = false; + V_ASSERT(p_token.isMovement()); - switch (p_movement) { + bool hasMoved = false; + bool inclusive = true; + bool forward = true; + + switch (p_token.m_movement) { case Movement::Left: { if (p_repeat == -1) { @@ -1765,6 +1936,29 @@ bool VVim::processMovement(QTextCursor &p_cursor, const QTextDocument *p_doc, break; } + case Movement::TillBackward: + forward = false; + // Fall through. + case Movement::TillForward: + inclusive = false; + goto handle_target; + + case Movement::FindBackward: + forward = false; + // Fall through. + case Movement::FindForward: + { +handle_target: + const Key &key = p_token.m_key; + QChar target = keyToChar(key.m_key, key.m_modifiers); + if (!target.isNull()) { + hasMoved = VEditUtils::findTargetWithinBlock(p_cursor, p_moveMode, + target, forward, inclusive); + } + + break; + } + default: break; } @@ -1979,7 +2173,7 @@ void VVim::processDeleteAction(QList &p_tokens) } cursor.beginEditBlock(); - hasMoved = processMovement(cursor, doc, moveMode, to.m_movement, repeat); + hasMoved = processMovement(cursor, doc, moveMode, to, repeat); if (repeat == -1) { repeat = 1; } @@ -2235,7 +2429,7 @@ void VVim::processCopyAction(QList &p_tokens) } cursor.beginEditBlock(); - changed = processMovement(cursor, doc, moveMode, to.m_movement, repeat); + changed = processMovement(cursor, doc, moveMode, to, repeat); if (repeat == -1) { repeat = 1; } @@ -2545,7 +2739,7 @@ void VVim::processChangeAction(QList &p_tokens) } cursor.beginEditBlock(); - hasMoved = processMovement(cursor, doc, moveMode, to.m_movement, repeat); + hasMoved = processMovement(cursor, doc, moveMode, to, repeat); if (repeat == -1) { repeat = 1; } @@ -2793,7 +2987,7 @@ void VVim::processIndentAction(QList &p_tokens, bool p_isIndent) processMovement(cursor, doc, QTextCursor::KeepAnchor, - to.m_movement, + to, repeat); VEditUtils::indentSelectedBlocks(doc, cursor, @@ -2852,7 +3046,7 @@ void VVim::processToLowerAction(QList &p_tokens, bool p_toLower) changed = processMovement(cursor, doc, moveMode, - to.m_movement, + to, repeat); if (repeat == -1) { repeat = 1; @@ -3051,6 +3245,19 @@ bool VVim::expectingRegisterName() const && m_keys.at(0) == Key(Qt::Key_QuoteDbl, Qt::ShiftModifier); } +bool VVim::expectingCharacterTarget() const +{ + if (m_keys.size() != 1) { + return false; + } + + const Key &key = m_keys.first(); + return (key == Key(Qt::Key_F, Qt::NoModifier) + || key == Key(Qt::Key_F, Qt::ShiftModifier) + || key == Key(Qt::Key_T, Qt::NoModifier) + || key == Key(Qt::Key_T, Qt::ShiftModifier)); +} + QChar VVim::keyToRegisterName(const Key &p_key) const { if (p_key.isAlphabet()) { @@ -3164,6 +3371,11 @@ void VVim::addMovementToken(Movement p_movement) m_tokens.append(Token(p_movement)); } +void VVim::addMovementToken(Movement p_movement, Key p_key) +{ + m_tokens.append(Token(p_movement, p_key)); +} + void VVim::deleteSelectedText(QTextCursor &p_cursor, bool p_clearEmptyBlock) { if (p_cursor.hasSelection()) { @@ -3303,3 +3515,44 @@ const QString &VVim::Register::read() return m_value; } + +void VVim::repeatLastFindMovement(bool p_reverse) +{ + if (!m_lastFindToken.isValid()) { + return; + } + + V_ASSERT(m_lastFindToken.isMovement()); + + Movement mm = m_lastFindToken.m_movement; + Key key = m_lastFindToken.m_key; + + V_ASSERT(key.isValid()); + + if (p_reverse) { + switch (mm) { + case Movement::FindForward: + mm = Movement::FindBackward; + break; + + case Movement::FindBackward: + mm = Movement::FindForward; + break; + + case Movement::TillForward: + mm = Movement::TillBackward; + break; + + case Movement::TillBackward: + mm = Movement::TillForward; + break; + + default: + break; + } + } + + tryAddMoveAction(); + addMovementToken(mm, key); + processCommand(m_tokens); +} diff --git a/src/utils/vvim.h b/src/utils/vvim.h index 1e1c90de..fa50be1a 100644 --- a/src/utils/vvim.h +++ b/src/utils/vvim.h @@ -56,6 +56,8 @@ private: { } + Key() : m_key(-1), m_modifiers(Qt::NoModifier) {} + int m_key; int m_modifiers; @@ -89,6 +91,11 @@ private: } } + bool isValid() const + { + return m_key > -1 && m_modifiers > -1; + } + bool operator==(const Key &p_key) const { return p_key.m_key == m_key && p_key.m_modifiers == m_modifiers; @@ -138,6 +145,10 @@ private: WORDBackward, BackwardEndOfWord, BackwardEndOfWORD, + FindForward, + FindBackward, + TillForward, + TillBackward, Invalid }; @@ -177,6 +188,9 @@ private: Token(Movement p_movement) : m_type(TokenType::Movement), m_movement(p_movement) {} + Token(Movement p_movement, Key p_key) + : m_type(TokenType::Movement), m_movement(p_movement), m_key(p_key) {} + Token(Range p_range) : m_type(TokenType::Range), m_range(p_range) {} @@ -202,6 +216,11 @@ private: return m_type == TokenType::Range; } + bool isValid() const + { + return m_type != TokenType::Invalid; + } + QString toString() const { QString str; @@ -235,9 +254,12 @@ private: { Action m_action; int m_repeat; - Movement m_movement; Range m_range; + Movement m_movement; }; + + // Used in some Movement. + Key m_key; }; struct Register @@ -358,6 +380,9 @@ private: // Check m_keys to see if we are expecting a register name. bool expectingRegisterName() const; + // Check m_keys to see if we are expecting a target for f/t/F/T command. + bool expectingCharacterTarget() 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; @@ -381,6 +406,9 @@ private: // Add an Movement token at the end of m_tokens. void addMovementToken(Movement p_movement); + // Add an Movement token at the end of m_tokens. + void addMovementToken(Movement p_movement, 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); @@ -401,11 +429,11 @@ private: // Remove QChar::ObjectReplacementCharacter before saving. void saveToRegister(const QString &p_text); - // Move @p_cursor according to @p_moveMode and @p_movement. + // Move @p_cursor according to @p_moveMode and @p_token. // Return true if it has moved @p_cursor. bool processMovement(QTextCursor &p_cursor, const QTextDocument *p_doc, QTextCursor::MoveMode p_moveMode, - Movement p_movement, int p_repeat); + const Token &p_token, int p_repeat); // Move @p_cursor according to @p_moveMode and @p_range. // Return true if it has moved @p_cursor. @@ -421,6 +449,9 @@ private: // Check if m_tokens only contains action token @p_action. bool checkActionToken(Action p_action) const; + // Repeat m_lastFindToken. + void repeatLastFindMovement(bool p_reverse); + VEdit *m_editor; const VEditConfig *m_editConfig; VimMode m_mode; @@ -440,6 +471,9 @@ private: // Currently used register. QChar m_regName; + // Last f/F/t/T Token. + Token m_lastFindToken; + static const QChar c_unnamedRegister; static const QChar c_blackHoleRegister; static const QChar c_selectionRegister;