#include "vvim.h" #include #include #include #include #include #include #include #include #include #include "vconfigmanager.h" #include "veditor.h" #include "utils/veditutils.h" #include "vconstants.h" #include "vmdeditor.h" extern VConfigManager *g_config; const QChar VVim::c_unnamedRegister = QChar('"'); const QChar VVim::c_blackHoleRegister = QChar('_'); const QChar VVim::c_selectionRegister = QChar('+'); QMap VVim::s_registers; const int VVim::SearchHistory::c_capacity = 50; #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_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 || VUtils::isControlModifierForVim(p_modifiers)) { 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; } #define ADDCHAR(x, y) case (x): {p_key = (y); break;} #define ADDCHAR_SHIFT(x, y) case (x): {p_key = (y); p_modifiers = Qt::ShiftModifier; break;} static void charToKey(const QChar &p_char, int &p_key, int &p_modifiers) { p_modifiers = Qt::NoModifier; ushort ucode = p_char.unicode(); if (ucode >= '0' && ucode <= '9') { p_key = Qt::Key_0 + ucode - '0'; return; } else if (ucode >= 'a' && ucode <= 'z') { p_key = Qt::Key_A + ucode - 'a'; return; } else if (ucode >= 'A' && ucode <= 'Z') { p_key = Qt::Key_A + ucode - 'A'; p_modifiers = Qt::ShiftModifier; return; } switch (p_char.unicode()) { ADDCHAR('\t', Qt::Key_Tab); ADDCHAR(' ', Qt::Key_Space); ADDCHAR_SHIFT('!', Qt::Key_Exclam); ADDCHAR_SHIFT('"', Qt::Key_QuoteDbl); ADDCHAR_SHIFT('#', Qt::Key_NumberSign); ADDCHAR_SHIFT('$', Qt::Key_Dollar); ADDCHAR_SHIFT('%', Qt::Key_Percent); ADDCHAR_SHIFT('&', Qt::Key_Ampersand); ADDCHAR('\'', Qt::Key_Apostrophe); ADDCHAR_SHIFT('(', Qt::Key_ParenLeft); ADDCHAR_SHIFT(')', Qt::Key_ParenRight); ADDCHAR_SHIFT('*', Qt::Key_Asterisk); ADDCHAR_SHIFT('+', Qt::Key_Plus); ADDCHAR(',', Qt::Key_Comma); ADDCHAR('-', Qt::Key_Minus); ADDCHAR('.', Qt::Key_Period); ADDCHAR('/', Qt::Key_Slash); ADDCHAR_SHIFT(':', Qt::Key_Colon); ADDCHAR(';', Qt::Key_Semicolon); ADDCHAR_SHIFT('<', Qt::Key_Less); ADDCHAR('=', Qt::Key_Equal); ADDCHAR_SHIFT('>', Qt::Key_Greater); ADDCHAR_SHIFT('?', Qt::Key_Question); ADDCHAR_SHIFT('@', Qt::Key_At); ADDCHAR('[', Qt::Key_BracketLeft); ADDCHAR('\\', Qt::Key_Backslash); ADDCHAR(']', Qt::Key_BracketRight); ADDCHAR_SHIFT('^', Qt::Key_AsciiCircum); ADDCHAR_SHIFT('_', Qt::Key_Underscore); ADDCHAR('`', Qt::Key_QuoteLeft); ADDCHAR_SHIFT('{', Qt::Key_BraceLeft); ADDCHAR_SHIFT('|', Qt::Key_Bar); ADDCHAR_SHIFT('}', Qt::Key_BraceRight); ADDCHAR_SHIFT('~', Qt::Key_AsciiTilde); default: p_key = -1; break; } } static QString keyToString(int p_key, int p_modifiers) { QChar ch = keyToChar(p_key, p_modifiers); if (ch.isNull()) { return QString(); } if (VUtils::isControlModifierForVim(p_modifiers)) { return QString("^") + ch; } else { return ch; } } VVim::VVim(VEditor *p_editor) : QObject(p_editor->getEditor()), m_editor(p_editor), m_editConfig(&p_editor->getConfig()), m_mode(VimMode::Invalid), m_resetPositionInBlock(true), m_regName(c_unnamedRegister), m_leaderKey(Key(Qt::Key_Space)), m_replayLeaderSequence(false), m_registerPending(false), m_insertModeAfterCommand(false), m_positionBeforeVisualMode(0) { Q_ASSERT(m_editConfig->m_enableVimMode); setMode(VimMode::Normal); initLeaderKey(); initRegisters(); connect(m_editor->object(), &VEditorObject::mousePressed, this, &VVim::handleMousePressed); connect(m_editor->object(), &VEditorObject::mouseMoved, this, &VVim::handleMouseMoved); connect(m_editor->object(), &VEditorObject::mouseReleased, this, &VVim::handleMouseReleased); } void VVim::initLeaderKey() { QChar ch = g_config->getVimLeaderKey(); Q_ASSERT(!ch.isNull()); int key, modifiers; charToKey(ch, key, modifiers); m_leaderKey = Key(key, modifiers); qDebug() << "Vim leader key" << ch << key << modifiers; } // Set @p_cursor's position specified by @p_positionInBlock. // If @p_positionInBlock is bigger than the block's length, move to the end of block. // Need to setTextCursor() after calling this. static void setCursorPositionInBlock(QTextCursor &p_cursor, int p_positionInBlock, QTextCursor::MoveMode p_mode) { QTextBlock block = p_cursor.block(); if (block.length() > p_positionInBlock) { p_cursor.setPosition(block.position() + p_positionInBlock, p_mode); } else { p_cursor.movePosition(QTextCursor::EndOfBlock, p_mode, 1); } } // Find the start and end of the spaces @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 spaces. // @p_start will equals to @p_end if @p_cursor is not a space. static void findCurrentSpace(const QTextCursor &p_cursor, int &p_start, int &p_end) { QTextBlock block = p_cursor.block(); QString text = block.text(); int pib = p_cursor.positionInBlock(); if (pib < text.size() && !text[pib].isSpace()) { p_start = p_end = p_cursor.position(); return; } p_start = 0; for (int i = pib - 1; i >= 0; --i) { if (!text[i].isSpace()) { p_start = i + 1; break; } } p_end = block.length() - 1; for (int i = pib; i < text.size(); ++i) { if (!text[i].isSpace()) { p_end = i; break; } } 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, bool p_stopAtBoundary = false) { 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() && !p_stopAtBoundary) { // 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 && !p_stopAtBoundary) { // 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; } } } } // Expand the selection of @p_cursor to contain additional spaces at the two ends // within a block. static void expandSelectionAcrossSpacesWithinBlock(QTextCursor &p_cursor) { QTextBlock block = p_cursor.block(); QString text = block.text(); int start = p_cursor.selectionStart() - block.position(); int end = p_cursor.selectionEnd() - block.position(); for (int i = start - 1; i >= 0; --i) { if (!text[i].isSpace()) { start = i + 1; break; } } for (int i = end; i < text.size(); ++i) { if (!text[i].isSpace()) { end = i; break; } } start += block.position(); end += block.position(); if (start == p_cursor.selectionStart() && end == p_cursor.selectionEnd()) { return; } if (p_cursor.anchor() <= p_cursor.position()) { p_cursor.setPosition(start, QTextCursor::MoveAnchor); p_cursor.setPosition(end, QTextCursor::KeepAnchor); } else { p_cursor.setPosition(end, QTextCursor::MoveAnchor); p_cursor.setPosition(start, QTextCursor::KeepAnchor); } } // In Change action, after deleting selected block text, we need to insert a new // block for user input. // @p_deletionStart is the global position of the start of the deletion. // Should be called immediately after the deletion. static void insertChangeBlockAfterDeletion(QTextCursor &p_cursor, int p_deletionStart) { if (p_cursor.position() < p_deletionStart) { // Insert a new block below. p_cursor.movePosition(QTextCursor::EndOfBlock); p_cursor.insertBlock(); } else { // Insert a new block above. p_cursor.movePosition(QTextCursor::StartOfBlock); p_cursor.insertBlock(); p_cursor.movePosition(QTextCursor::PreviousBlock); } if (g_config->getAutoIndent()) { VEditUtils::indentBlockAsBlock(p_cursor, false); } } // Given the percentage of the text, return the corresponding block number. // Notice that the block number is based on 0. // Returns -1 if it is not valid. static int percentageToBlockNumber(const QTextDocument *p_doc, int p_percent) { if (p_percent > 100 || p_percent <= 0) { return -1; } int nrBlock = p_doc->blockCount(); int num = nrBlock * (p_percent * 1.0 / 100) - 1; return num >= 0 ? num : 0; } // 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; } // Reverse the case of selected text. // Returns true if the reverse has taken place. // Need to setTextCursor() after calling this. static bool reverseSelectedTextCase(QTextCursor &p_cursor) { if (!p_cursor.hasSelection()) { return false; } QTextDocument *doc = p_cursor.document(); 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 { QChar ch = doc->characterAt(p_cursor.position()); bool changed = false; if (ch.isLower()) { ch = ch.toUpper(); changed = true; } else if (ch.isUpper()) { ch = ch.toLower(); changed = true; } if (changed) { p_cursor.deleteChar(); // insertText() will move the cursor right after the inserted text. p_cursor.insertText(ch); } else { p_cursor.movePosition(QTextCursor::NextCharacter); } } } return true; } // Join current cursor line and the next line. static void joinTwoLines(QTextCursor &p_cursor, bool p_modifySpaces) { QTextDocument *doc = p_cursor.document(); QTextBlock firstBlock = p_cursor.block(); QString textToAppend = firstBlock.next().text(); p_cursor.movePosition(QTextCursor::EndOfBlock); if (p_modifySpaces) { bool insertSpaces = false; if (firstBlock.length() > 1) { QChar lastChar = doc->characterAt(p_cursor.position() - 1); if (!lastChar.isSpace()) { insertSpaces = true; } } if (insertSpaces) { p_cursor.insertText(" "); } // Remove indentation. int idx = 0; for (idx = 0; idx < textToAppend.size(); ++idx) { if (!textToAppend[idx].isSpace()) { break; } } textToAppend = textToAppend.right(textToAppend.size() - idx); } // Now p_cursor is at the end of the first block. int position = p_cursor.block().position() + p_cursor.positionInBlock(); p_cursor.insertText(textToAppend); // Delete the second block. p_cursor.movePosition(QTextCursor::NextBlock); VEditUtils::removeBlock(p_cursor); // Position p_cursor right at the front of appended text. p_cursor.setPosition(position); } // Join lines specified by [@p_firstBlock, @p_firstBlock + p_blockCount). // Need to check the block range (based on 0). static bool joinLines(QTextCursor &p_cursor, int p_firstBlock, int p_blockCount, bool p_modifySpaces) { QTextDocument *doc = p_cursor.document(); int totalBlockCount = doc->blockCount(); if (p_blockCount <= 0 || p_firstBlock >= totalBlockCount - 1) { return false; } p_blockCount = qMin(p_blockCount, totalBlockCount - p_firstBlock); p_cursor.setPosition(doc->findBlockByNumber(p_firstBlock).position()); for (int i = 1; i < p_blockCount; ++i) { joinTwoLines(p_cursor, p_modifySpaces); } return true; } bool VVim::handleKeyPressEvent(QKeyEvent *p_event, int *p_autoIndentPos) { bool ret = handleKeyPressEvent(p_event->key(), p_event->modifiers(), p_autoIndentPos); if (ret) { p_event->accept(); } return ret; } bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) { bool ret = false; bool resetPositionInBlock = true; Key keyInfo(key, modifiers); bool unindent = false; int autoIndentPos = p_autoIndentPos ? *p_autoIndentPos : -1; // Handle Insert mode key press. if (VimMode::Insert == m_mode) { if (checkEnterNormalMode(key, modifiers)) { // See if we need to cancel auto indent. bool cancelAutoIndent = false; if (p_autoIndentPos && *p_autoIndentPos > -1) { QTextCursor cursor = m_editor->textCursorW(); cancelAutoIndent = VEditUtils::needToCancelAutoIndent(*p_autoIndentPos, cursor); if (cancelAutoIndent) { autoIndentPos = -1; VEditUtils::deleteIndentAndListMark(cursor); m_editor->setTextCursorW(cursor); } } // Clear selection and enter Normal mode. if (!cancelAutoIndent) { clearSelection(); } setMode(VimMode::Normal); goto clear_accept; } if (m_registerPending) { // Ctrl and Shift may be sent out first. if (key == Qt::Key_Control || key == Qt::Key_Shift || key == Qt::Key_Meta) { goto accept; } // Expecting a register name. QChar reg = keyToRegisterName(keyInfo); if (!reg.isNull()) { // Insert register content. m_editor->insertPlainTextW(getRegister(reg).read()); } goto clear_accept; } else if (key == Qt::Key_R && VUtils::isControlModifierForVim(modifiers)) { // Ctrl+R, insert the content of a register. m_pendingKeys.append(keyInfo); m_registerPending = true; goto accept; } if (key == Qt::Key_O && VUtils::isControlModifierForVim(modifiers)) { // Ctrl+O, enter normal mode, execute one command, then return to insert mode. m_insertModeAfterCommand = true; clearSelection(); setMode(VimMode::Normal); goto accept; } // Let it be handled outside VVim. goto exit; } // Ctrl and Shift may be sent out first. if (key == Qt::Key_Control || key == Qt::Key_Shift || key == Qt::Key_Meta) { goto accept; } if (m_replayLeaderSequence) { qDebug() << "replaying sequence" << keyToChar(key, modifiers); } m_pendingKeys.append(keyInfo); if (expectingLeaderSequence()) { if (processLeaderSequence(keyInfo)) { goto accept; } else { goto clear_accept; } } if (expectingRegisterName()) { // Expecting a register name. QChar reg = keyToRegisterName(keyInfo); if (!reg.isNull()) { m_keys.clear(); setCurrentRegisterName(reg); Register &r = getRegister(reg); if (r.isNamedRegister()) { r.m_append = (modifiers == Qt::ShiftModifier); } else { Q_ASSERT(!r.m_append); } goto accept; } goto clear_accept; } if (expectingMarkName()) { // Expecting a mark name to create a mark. if (keyInfo.isAlphabet() && modifiers == Qt::NoModifier) { m_keys.clear(); m_marks.setMark(keyToChar(key, modifiers), m_editor->textCursorW()); } goto clear_accept; } if (expectingMarkTarget()) { // Expecting a mark name as the target. Movement mm = Movement::Invalid; const Key &aKey = m_keys.first(); if (aKey == Key(Qt::Key_Apostrophe)) { mm = Movement::MarkJumpLine; } else { Q_ASSERT(aKey == Key(Qt::Key_QuoteLeft)); mm = Movement::MarkJump; } tryAddMoveAction(); addMovementToken(mm, keyInfo); processCommand(m_tokens); 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; } } tryAddMoveAction(); addMovementToken(mm, keyInfo); m_lastFindToken = m_tokens.last(); processCommand(m_tokens); 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. if (keyInfo == m_leaderKey && !hasActionToken() && !hasNonDigitPendingKeys() && !m_replayLeaderSequence) { tryGetRepeatToken(m_keys, m_tokens); Q_ASSERT(m_keys.isEmpty()); m_pendingKeys.pop_back(); m_pendingKeys.append(Key(Qt::Key_Backslash)); m_keys.append(Key(Qt::Key_Backslash)); goto 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) { case Qt::Key_0: { if (modifiers == Qt::NoModifier || modifiers == Qt::KeypadModifier) { if (checkPendingKey(Key(Qt::Key_G))) { // StartOfVisualLine. tryAddMoveAction(); m_tokens.append(Token(Movement::StartOfVisualLine)); processCommand(m_tokens); } else if (m_keys.isEmpty()) { // StartOfLine. tryAddMoveAction(); m_tokens.append(Token(Movement::StartOfLine)); processCommand(m_tokens); } else if (m_keys.last().isDigit()) { // Repeat. m_keys.append(keyInfo); resetPositionInBlock = false; goto accept; } } break; } case Qt::Key_1: case Qt::Key_2: case Qt::Key_3: case Qt::Key_4: case Qt::Key_5: case Qt::Key_6: case Qt::Key_7: case Qt::Key_8: case Qt::Key_9: { if (modifiers == Qt::NoModifier || modifiers == Qt::KeypadModifier) { if (!m_keys.isEmpty() && numberFromKeySequence(m_keys) == -1) { // Invalid sequence. break; } m_keys.append(keyInfo); resetPositionInBlock = false; goto accept; } break; } case Qt::Key_Left: case Qt::Key_Down: case Qt::Key_Up: case Qt::Key_Right: case Qt::Key_H: case Qt::Key_J: case Qt::Key_K: case Qt::Key_L: { if (modifiers == Qt::NoModifier || modifiers == Qt::KeypadModifier) { // Check if we could generate a Repeat token. tryGetRepeatToken(m_keys, m_tokens); // Generate a Movement token. Movement mm = Movement::Invalid; if (!m_keys.isEmpty()) { // gj, gk. Key gKey(Qt::Key_G); if (m_keys.size() == 1 && m_keys.at(0) == gKey) { if (key == Qt::Key_J) { mm = Movement::VisualDown; } else if (key == Qt::Key_K) { mm = Movement::VisualUp; } else { break; } } else { // Not a valid sequence. break; } } else { // h, j, k, l. switch (key) { case Qt::Key_H: case Qt::Key_Left: mm = Movement::Left; break; case Qt::Key_L: case Qt::Key_Right: mm = Movement::Right; break; case Qt::Key_J: case Qt::Key_Down: mm = Movement::Down; break; case Qt::Key_K: case Qt::Key_Up: mm = Movement::Up; break; default: V_ASSERT(false); } } V_ASSERT(mm != Movement::Invalid); tryAddMoveAction(); addMovementToken(mm); processCommand(m_tokens); resetPositionInBlock = false; } else if (modifiers == Qt::ShiftModifier) { if (key == Qt::Key_J) { tryGetRepeatToken(m_keys, m_tokens); if (hasActionToken()) { break; } if (checkPendingKey(Key(Qt::Key_G))) { // gJ, JoinNoModification. addActionToken(Action::JoinNoModification); } else if (m_keys.isEmpty()) { // J, Join. addActionToken(Action::Join); } processCommand(m_tokens); } } else if (VUtils::isControlModifierForVim(modifiers)) { if (key == Qt::Key_J || key == Qt::Key_K) { // Let it be handled outside. resetState(); goto exit; } } break; } case Qt::Key_I: { if (modifiers == Qt::NoModifier) { if (hasActionTokenValidForTextObject()) { // Inner text object. tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty()) { // Invalid sequence; break; } m_keys.append(keyInfo); goto accept; } // Enter Insert mode. // Different from Vim: // We enter Insert mode even in Visual and VisualLine mode. We // also keep the selection after the mode change. if (checkMode(VimMode::Normal) || checkMode(VimMode::Visual) || checkMode(VimMode::VisualLine)) { setMode(VimMode::Insert, false); } } else if (modifiers == Qt::ShiftModifier) { QTextCursor cursor = m_editor->textCursorW(); if (m_mode == VimMode::Normal) { // Insert at the first non-space character. VEditUtils::moveCursorFirstNonSpaceCharacter(cursor, QTextCursor::MoveAnchor); } else if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) { // Insert at the start of line. cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor, 1); } m_editor->setTextCursorW(cursor); setMode(VimMode::Insert); } else if (VUtils::isControlModifierForVim(modifiers)) { // Ctrl+I, jump to next location. if (!m_tokens.isEmpty() || !checkMode(VimMode::Normal)) { break; } tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty()) { break; } addActionToken(Action::JumpNextLocation); processCommand(m_tokens); break; } break; } case Qt::Key_A: { if (modifiers == Qt::NoModifier) { if (hasActionTokenValidForTextObject()) { // Around text object. tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty()) { // Invalid sequence; break; } m_keys.append(keyInfo); goto accept; } // Enter Insert mode. // Move cursor back one character. if (m_mode == VimMode::Normal) { QTextCursor cursor = m_editor->textCursorW(); V_ASSERT(!cursor.hasSelection()); if (!cursor.atBlockEnd()) { cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 1); m_editor->setTextCursorW(cursor); } setMode(VimMode::Insert); } } else if (modifiers == Qt::ShiftModifier) { // Insert at the end of line. QTextCursor cursor = m_editor->textCursorW(); if (m_mode == VimMode::Normal) { cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor, 1); m_editor->setTextCursorW(cursor); } else if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) { if (!cursor.atBlockEnd()) { cursor.clearSelection(); cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 1); m_editor->setTextCursorW(cursor); } } setMode(VimMode::Insert); } break; } case Qt::Key_O: { if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) { // Insert a new block under/above current block and enter insert mode. bool insertAbove = modifiers == Qt::ShiftModifier; if (m_mode == VimMode::Normal) { QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); cursor.movePosition(insertAbove ? QTextCursor::StartOfBlock : QTextCursor::EndOfBlock, QTextCursor::MoveAnchor, 1); cursor.insertBlock(); if (insertAbove) { cursor.movePosition(QTextCursor::PreviousBlock, QTextCursor::MoveAnchor, 1); } bool textInserted = false; if (g_config->getAutoIndent()) { textInserted = VEditUtils::indentBlockAsBlock(cursor, false); bool listInserted = false; if (g_config->getAutoList()) { listInserted = VEditUtils::insertListMarkAsPreviousBlock(cursor); textInserted = listInserted || textInserted; } if (!listInserted && g_config->getAutoQuote()) { textInserted = VEditUtils::insertQuoteMarkAsPreviousBlock(cursor) || textInserted; } } cursor.endEditBlock(); m_editor->setTextCursorW(cursor); if (textInserted) { autoIndentPos = cursor.position(); } setMode(VimMode::Insert); } break; } else if (VUtils::isControlModifierForVim(modifiers)) { // Ctrl+O, jump to previous location. if (!m_tokens.isEmpty() || !checkMode(VimMode::Normal)) { break; } tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty()) { break; } addActionToken(Action::JumpPreviousLocation); processCommand(m_tokens); break; } break; } case Qt::Key_S: { tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty() || hasActionToken()) { break; } if (modifiers == Qt::NoModifier) { addActionToken(Action::Change); // Movement will be ignored in Visual mode. addMovementToken(Movement::Right); processCommand(m_tokens); } else if (modifiers == Qt::ShiftModifier) { // S, change current line. addActionToken(Action::Change); addRangeToken(Range::Line); processCommand(m_tokens); } break; } case Qt::Key_Dollar: { if (modifiers == Qt::ShiftModifier) { // $, move to end of line. tryGetRepeatToken(m_keys, m_tokens); if (m_keys.isEmpty()) { tryAddMoveAction(); m_tokens.append(Token(Movement::EndOfLine)); processCommand(m_tokens); } } break; } case Qt::Key_G: { Movement mm = Movement::Invalid; if (modifiers == Qt::NoModifier) { tryGetRepeatToken(m_keys, m_tokens); if (m_keys.isEmpty()) { // First g, pend it. m_keys.append(keyInfo); goto accept; } else if (m_keys.size() == 1 && m_keys.at(0) == keyInfo) { // gg, go to a certain line or first line. if (!m_tokens.isEmpty() && m_tokens.last().isRepeat()) { mm = Movement::LineJump; } else { mm = Movement::StartOfDocument; } } } else if (modifiers == Qt::ShiftModifier) { tryGetRepeatToken(m_keys, m_tokens); if (m_keys.isEmpty()) { // G, go to a certain line or the last line. if (!m_tokens.isEmpty() && m_tokens.last().isRepeat()) { mm = Movement::LineJump; } else { mm = Movement::EndOfDocument; } } } if (mm != Movement::Invalid) { tryAddMoveAction(); m_tokens.append(Token(mm)); processCommand(m_tokens); } break; } // Should be kept together with Qt::Key_PageUp. case Qt::Key_B: { if (VUtils::isControlModifierForVim(modifiers)) { // 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()) { if (modifiers == Qt::NoModifier && checkPendingKey(Key(Qt::Key_Z))) { // zb, redraw to make a certain line the bottom of window. addActionToken(Action::RedrawAtBottom); processCommand(m_tokens); break; } 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; } tryAddMoveAction(); m_tokens.append(Token(mm)); processCommand(m_tokens); break; } else { break; } V_FALLTHROUGH; } case Qt::Key_PageUp: { if (modifiers == Qt::NoModifier) { tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty()) { // Not a valid sequence. break; } Movement mm = Movement::PageUp; tryAddMoveAction(); m_tokens.append(Token(mm)); processCommand(m_tokens); resetPositionInBlock = false; } break; } case Qt::Key_U: { tryGetRepeatToken(m_keys, m_tokens); bool toLower = modifiers == Qt::NoModifier; if (VUtils::isControlModifierForVim(modifiers)) { // Ctrl+U, HalfPageUp. if (!m_keys.isEmpty()) { // Not a valid sequence. break; } Movement mm = Movement::HalfPageUp; tryAddMoveAction(); m_tokens.append(Token(mm)); processCommand(m_tokens); resetPositionInBlock = false; } else if (m_keys.isEmpty() && !hasActionToken()) { if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) { // u/U for tolower and toupper selected text. QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); // Different from Vim: // If there is no selection in Visual mode, we do nothing. if (m_mode == VimMode::VisualLine) { int nrBlock = VEditUtils::selectedBlockCount(cursor); message(tr("%1 %2 changed").arg(nrBlock).arg(nrBlock > 1 ? tr("lines") : tr("line"))); } convertCaseOfSelectedText(cursor, toLower); cursor.endEditBlock(); m_editor->setTextCursorW(cursor); setMode(VimMode::Normal); break; } // u, Undo. if (modifiers == Qt::NoModifier) { addActionToken(Action::Undo); processCommand(m_tokens); } break; } else { if (hasActionToken()) { // guu/gUU. if ((toLower && checkActionToken(Action::ToLower)) || (!toLower && checkActionToken(Action::ToUpper))) { addRangeToken(Range::Line); processCommand(m_tokens); break; } else { // An invalid sequence. break; } } else if (checkPendingKey(Key(Qt::Key_G))) { // gu/gU, ToLower/ToUpper action. if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) { QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); // Different from Vim: // If there is no selection in Visual mode, we do nothing. if (m_mode == VimMode::VisualLine) { int nrBlock = VEditUtils::selectedBlockCount(cursor); message(tr("%1 %2 changed").arg(nrBlock).arg(nrBlock > 1 ? tr("lines") : tr("line"))); } convertCaseOfSelectedText(cursor, toLower); cursor.endEditBlock(); m_editor->setTextCursorW(cursor); setMode(VimMode::Normal); break; } addActionToken(toLower ? Action::ToLower : Action::ToUpper); m_keys.clear(); goto accept; } else { // An invalid sequence. break; } } break; } // Ctrl+F is used for Find dialog, not used here. case Qt::Key_PageDown: { if (modifiers == Qt::NoModifier) { tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty()) { // Not a valid sequence. break; } Movement mm = Movement::PageDown; tryAddMoveAction(); m_tokens.append(Token(mm)); processCommand(m_tokens); resetPositionInBlock = false; } break; } case Qt::Key_D: { if (VUtils::isControlModifierForVim(modifiers)) { // Ctrl+D, HalfPageDown. tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty()) { // Not a valid sequence. break; } Movement mm = Movement::HalfPageDown; tryAddMoveAction(); m_tokens.append(Token(mm)); processCommand(m_tokens); resetPositionInBlock = false; } else if (modifiers == Qt::NoModifier) { // d, delete action. tryGetRepeatToken(m_keys, m_tokens); if (hasActionToken()) { // This is another d, something like dd. if (checkActionToken(Action::Delete)) { addRangeToken(Range::Line); processCommand(m_tokens); break; } else { // An invalid sequence. break; } } else { // The first d, an Action. addActionToken(Action::Delete); if (checkMode(VimMode::Visual) || checkMode(VimMode::VisualLine)) { // Movement will be ignored. addMovementToken(Movement::Left); processCommand(m_tokens); setMode(VimMode::Normal); break; } goto accept; } } else if (modifiers == Qt::ShiftModifier) { tryGetRepeatToken(m_keys, m_tokens); if (!hasActionToken()) { if (checkMode(VimMode::Normal)) { // D, same as d$. addActionToken(Action::Delete); addMovementToken(Movement::EndOfLine); processCommand(m_tokens); } else if (checkMode(VimMode::Visual) || checkMode(VimMode::VisualLine)) { // D, same as dd. addActionToken(Action::Delete); addRangeToken(Range::Line); processCommand(m_tokens); setMode(VimMode::Normal); } } break; } break; } case Qt::Key_BracketRight: { if (modifiers == Qt::NoModifier) { tryGetRepeatToken(m_keys, m_tokens); if (checkPendingKey(Key(Qt::Key_I)) || checkPendingKey(Key(Qt::Key_A))) { // BracketInner/BracketAround. Range range = Range::BracketInner; if (checkPendingKey(Key(Qt::Key_A))) { range = Range::BracketAround; } addRangeToken(range); processCommand(m_tokens); break; } else if (hasActionToken() || !checkMode(VimMode::Normal)) { // Invalid sequence. break; } else if (m_keys.isEmpty()) { // First ], pend it. m_keys.append(keyInfo); goto accept; } else if (checkPendingKey(keyInfo)) { // ]], goto next title, regardless of level. processTitleJump(m_tokens, true, 1); } else if (checkPendingKey(Key(Qt::Key_BracketLeft))) { // [], goto previous title at the same level. processTitleJump(m_tokens, false, 0); } break; } break; } // Should be kept together with Qt::Key_Escape. case Qt::Key_BracketLeft: { if (VUtils::isControlModifierForVim(modifiers)) { clearSelectionAndEnterNormalMode(); } else if (modifiers == Qt::NoModifier) { tryGetRepeatToken(m_keys, m_tokens); if (checkPendingKey(Key(Qt::Key_I)) || checkPendingKey(Key(Qt::Key_A))) { // BracketInner/BracketAround. Range range = Range::BracketInner; if (checkPendingKey(Key(Qt::Key_A))) { range = Range::BracketAround; } addRangeToken(range); processCommand(m_tokens); break; } else if (hasActionToken() || !checkMode(VimMode::Normal)) { // Invalid sequence. break; } else if (m_keys.isEmpty()) { // First [, pend it. m_keys.append(keyInfo); goto accept; } else if (checkPendingKey(keyInfo)) { // [[, goto previous title, regardless of level. processTitleJump(m_tokens, false, 1); } else if (checkPendingKey(Key(Qt::Key_BracketRight))) { // ][, goto next title at the same level. processTitleJump(m_tokens, true, 0); } } break; } case Qt::Key_Escape: { clearSelectionAndEnterNormalMode(); break; } case Qt::Key_V: { if (modifiers == Qt::NoModifier) { if (checkMode(VimMode::Visual)) { setMode(VimMode::Normal, true); } else { // Toggle Visual Mode. setMode(VimMode::Visual); maintainSelectionInVisualMode(); } } else if (modifiers == Qt::ShiftModifier) { // Visual Line Mode. clearSelection(); VimMode mode = VimMode::VisualLine; if (m_mode == VimMode::VisualLine) { mode = VimMode::Normal; } setMode(mode); if (m_mode == VimMode::VisualLine) { QTextCursor cursor = m_editor->textCursorW(); expandSelectionToWholeLines(cursor); m_editor->setTextCursorW(cursor); } } else if (VUtils::isControlModifierForVim(modifiers)) { if (g_config->getVimExemptionKeys().contains('v')) { // Let it be handled outside. resetState(); goto exit; } } break; } case Qt::Key_AsciiCircum: { if (modifiers == Qt::ShiftModifier) { // ^, go to first non-space character of current line (block). tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty()) { // Not a valid sequence. break; } Movement mm = Movement::FirstCharacter; tryAddMoveAction(); m_tokens.append(Token(mm)); processCommand(m_tokens); } break; } case Qt::Key_W: { if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) { bool shift = modifiers == Qt::ShiftModifier; tryGetRepeatToken(m_keys, m_tokens); if (checkPendingKey(Key(Qt::Key_I)) || checkPendingKey(Key(Qt::Key_A))) { // WordInner/WORDInner/WordAournd/WORDAround. bool around = checkPendingKey(Key(Qt::Key_A)); Range range = Range::Invalid; if (shift) { if (around) { range = Range::WORDAround; } else { range = Range::WORDInner; } } else { if (around) { range = Range::WordAround; } else { range = Range::WordInner; } } addRangeToken(range); processCommand(m_tokens); break; } else if (!m_keys.isEmpty()) { // Not a valid sequence. break; } // w, go to the start of next word. Movement mm = Movement::WordForward; if (shift) { // W, go to the start of next WORD. mm = Movement::WORDForward; } if (checkActionToken(Action::Change)) { // In Change action, cw equals to ce. if (shift) { mm = Movement::ForwardEndOfWORD; } else { mm = Movement::ForwardEndOfWord; } } else { tryAddMoveAction(); } addMovementToken(mm); processCommand(m_tokens); break; } 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; } } tryAddMoveAction(); m_tokens.append(Token(mm)); processCommand(m_tokens); } break; } case Qt::Key_QuoteDbl: { if (modifiers == Qt::ShiftModifier) { tryGetRepeatToken(m_keys, m_tokens); if (checkPendingKey(Key(Qt::Key_I)) || checkPendingKey(Key(Qt::Key_A))) { // DoubleQuoteInner/DoubleQuoteAround. Range range = Range::DoubleQuoteInner; if (checkPendingKey(Key(Qt::Key_A))) { range = Range::DoubleQuoteAround; } addRangeToken(range); processCommand(m_tokens); break; } else if (!m_keys.isEmpty() || hasActionToken()) { // Invalid sequence. break; } // ", specify a register. m_keys.append(keyInfo); goto accept; } break; } case Qt::Key_X: { if (modifiers == Qt::ShiftModifier || modifiers == Qt::NoModifier) { // x, or X to delete one char. tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty() || hasActionToken()) { break; } addActionToken(Action::Delete); if (modifiers == Qt::ShiftModifier) { if (checkMode(VimMode::Visual) || checkMode(VimMode::VisualLine)) { // X, same as dd. addRangeToken(Range::Line); } else { // X, delete one char. addMovementToken(Movement::Left); } } else { // x. // Movement will be ignored in Visual mode. addMovementToken(Movement::Right); } processCommand(m_tokens); setMode(VimMode::Normal); break; } break; } case Qt::Key_Y: { if (modifiers == Qt::NoModifier) { // y, copy action. tryGetRepeatToken(m_keys, m_tokens); if (hasActionToken()) { // This is another y, something like yy. if (checkActionToken(Action::Copy) && checkMode(VimMode::Normal)) { addRangeToken(Range::Line); processCommand(m_tokens); } else { // An invalid sequence. break; } } else { // The first y, an Action. if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) { copySelectedText(m_mode == VimMode::VisualLine); setMode(VimMode::Normal); break; } addActionToken(Action::Copy); goto accept; } } else if (modifiers == Qt::ShiftModifier) { tryGetRepeatToken(m_keys, m_tokens); if (!hasActionToken()) { // Y, same as yy. addActionToken(Action::Copy); addRangeToken(Range::Line); processCommand(m_tokens); setMode(VimMode::Normal); } break; } break; } case Qt::Key_P: { if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) { // p/P, paste/pastebefore action. tryGetRepeatToken(m_keys, m_tokens); if (hasActionToken() || !m_keys.isEmpty()) { // An invalid sequence. break; } addActionToken(modifiers == Qt::NoModifier ? Action::Paste : Action::PasteBefore); processCommand(m_tokens); break; } break; } case Qt::Key_C: { if (modifiers == Qt::NoModifier) { // c, change action. tryGetRepeatToken(m_keys, m_tokens); if (hasActionToken()) { // This is another c, something like cc. if (checkActionToken(Action::Change)) { addRangeToken(Range::Line); processCommand(m_tokens); } } else { // The first c, an action. addActionToken(Action::Change); if (checkMode(VimMode::VisualLine) || checkMode(VimMode::Visual)) { // Movement will be ignored. addMovementToken(Movement::Left); processCommand(m_tokens); break; } goto accept; } } else if (modifiers == Qt::ShiftModifier) { tryGetRepeatToken(m_keys, m_tokens); if (!hasActionToken() && m_mode == VimMode::Normal) { // C, same as c$. addActionToken(Action::Change); addMovementToken(Movement::EndOfLine); processCommand(m_tokens); } } else if (VUtils::isControlModifierForVim(modifiers)) { if (g_config->getVimExemptionKeys().contains('c')) { // Let it be handled outside. resetState(); goto exit; } else { clearSelectionAndEnterNormalMode(); } } break; } case Qt::Key_Less: unindent = true; // Fall through. case Qt::Key_Greater: { if (modifiers == Qt::ShiftModifier) { // >/<, Indent/Unindent. tryGetRepeatToken(m_keys, m_tokens); if (hasActionToken()) { // This is another >/<, something like >>/<<. if ((!unindent && checkActionToken(Action::Indent)) || (unindent && checkActionToken(Action::UnIndent))) { addRangeToken(Range::Line); processCommand(m_tokens); break; } else if (checkPendingKey(Key(Qt::Key_I)) || checkPendingKey(Key(Qt::Key_A))) { // AngleBracketInner/AngleBracketAround. Range range = Range::AngleBracketInner; if (checkPendingKey(Key(Qt::Key_A))) { range = Range::AngleBracketAround; } addRangeToken(range); processCommand(m_tokens); break; } } else { // The first >/<, an Action. addActionToken(unindent ? Action::UnIndent : Action::Indent); if (checkMode(VimMode::Visual) || checkMode(VimMode::VisualLine)) { // Movement will be ignored. addMovementToken(Movement::Left); processCommand(m_tokens); break; } goto accept; } } break; } case Qt::Key_Equal: { if (modifiers == Qt::NoModifier) { // =, AutoIndent. tryGetRepeatToken(m_keys, m_tokens); if (hasActionToken()) { // ==. if (checkActionToken(Action::AutoIndent)) { addRangeToken(Range::Line); processCommand(m_tokens); break; } } else { // The first =, an Action. addActionToken(Action::AutoIndent); if (checkMode(VimMode::Visual) || checkMode(VimMode::VisualLine)) { // Movement will be ignored. addMovementToken(Movement::Left); processCommand(m_tokens); break; } goto accept; } } 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; } else if (modifiers == Qt::NoModifier && checkPendingKey(Key(Qt::Key_Z))) { // zt, redraw to make a certain line the top of window. addActionToken(Action::RedrawAtTop); processCommand(m_tokens); break; } 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; } case Qt::Key_R: { if (m_mode == VimMode::VisualLine) { break; } if (VUtils::isControlModifierForVim(modifiers)) { // Redo. tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty() || hasActionToken()) { break; } 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; } case Qt::Key_Z: { if (modifiers == Qt::NoModifier) { tryGetRepeatToken(m_keys, m_tokens); if (m_keys.isEmpty() && !hasActionToken()) { // First z, pend it. m_keys.append(keyInfo); goto accept; } else if (checkPendingKey(keyInfo)) { // zz, redraw to make a certain line the center of the window. addActionToken(Action::RedrawAtCenter); processCommand(m_tokens); break; } break; } break; } case Qt::Key_Colon: { if (modifiers == Qt::ShiftModifier) { if (m_keys.isEmpty() && m_tokens.isEmpty() && checkMode(VimMode::Normal)) { emit commandLineTriggered(CommandLineType::Command); } } break; } case Qt::Key_Percent: { if (modifiers == Qt::ShiftModifier) { if (m_keys.isEmpty()) { // %, FindPair movement. tryAddMoveAction(); addMovementToken(Movement::FindPair); processCommand(m_tokens); break; } else { tryGetRepeatToken(m_keys, m_tokens); if (m_keys.isEmpty() && hasRepeatToken()) { // xx%, jump to a certain line (percentage of the documents). // Change the repeat from percentage to line number. Token *token = getRepeatToken(); int bn = percentageToBlockNumber(m_editor->documentW(), token->m_repeat); if (bn == -1) { break; } else { // Repeat of LineJump is based on 1. token->m_repeat = bn + 1; } tryAddMoveAction(); addMovementToken(Movement::LineJump); processCommand(m_tokens); break; } } break; } break; } case Qt::Key_M: { if (modifiers == Qt::NoModifier) { if (m_keys.isEmpty() && m_tokens.isEmpty()) { // m, creating a mark. // We can create marks in Visual mode, too. m_keys.append(keyInfo); goto accept; } } break; } case Qt::Key_Apostrophe: { if (modifiers == Qt::NoModifier) { tryGetRepeatToken(m_keys, m_tokens); if (checkPendingKey(Key(Qt::Key_I)) || checkPendingKey(Key(Qt::Key_A))) { // QuoteInner/QuoteAround. Range range = Range::QuoteInner; if (checkPendingKey(Key(Qt::Key_A))) { range = Range::QuoteAround; } addRangeToken(range); processCommand(m_tokens); break; } // ', jump to the start of line of a mark. // Repeat is useless in this case. if (m_keys.isEmpty()) { m_keys.append(keyInfo); goto accept; } } break; } case Qt::Key_QuoteLeft: { if (modifiers == Qt::NoModifier) { tryGetRepeatToken(m_keys, m_tokens); if (checkPendingKey(Key(Qt::Key_I)) || checkPendingKey(Key(Qt::Key_A))) { // BackQuoteInner/BackQuoteAround. Range range = Range::BackQuoteInner; if (checkPendingKey(Key(Qt::Key_A))) { range = Range::BackQuoteAround; } addRangeToken(range); processCommand(m_tokens); break; } // `, jump to a mark. // Repeat is useless in this case. if (m_keys.isEmpty()) { m_keys.append(keyInfo); goto accept; } } break; } case Qt::Key_ParenLeft: // Fall through. case Qt::Key_ParenRight: { if (modifiers == Qt::ShiftModifier) { tryGetRepeatToken(m_keys, m_tokens); if (checkPendingKey(Key(Qt::Key_I)) || checkPendingKey(Key(Qt::Key_A))) { // ParenthesisInner/ParenthesisAround. Range range = Range::ParenthesisInner; if (checkPendingKey(Key(Qt::Key_A))) { range = Range::ParenthesisAround; } addRangeToken(range); processCommand(m_tokens); break; } } break; } case Qt::Key_BraceLeft: { if (modifiers == Qt::ShiftModifier) { tryGetRepeatToken(m_keys, m_tokens); if (checkPendingKey(Key(Qt::Key_I)) || checkPendingKey(Key(Qt::Key_A))) { // BraceInner/BraceAround. Range range = Range::BraceInner; if (checkPendingKey(Key(Qt::Key_A))) { range = Range::BraceAround; } addRangeToken(range); processCommand(m_tokens); break; } else if (m_keys.isEmpty()) { // {, ParagraphUp movement. tryAddMoveAction(); addMovementToken(Movement::ParagraphUp); processCommand(m_tokens); } else if (!hasActionToken() && checkPendingKey(Key(Qt::Key_BracketLeft))) { // [{, goto previous title at one higher level. if (checkMode(VimMode::Normal)) { processTitleJump(m_tokens, false, -1); } } break; } break; } case Qt::Key_BraceRight: { if (modifiers == Qt::ShiftModifier) { tryGetRepeatToken(m_keys, m_tokens); if (checkPendingKey(Key(Qt::Key_I)) || checkPendingKey(Key(Qt::Key_A))) { // BraceInner/BraceAround. Range range = Range::BraceInner; if (checkPendingKey(Key(Qt::Key_A))) { range = Range::BraceAround; } addRangeToken(range); processCommand(m_tokens); break; } else if (m_keys.isEmpty()) { // }, ParagraphDown movement. tryAddMoveAction(); addMovementToken(Movement::ParagraphDown); processCommand(m_tokens); } else if (!hasActionToken() && checkPendingKey(Key(Qt::Key_BracketRight))) { // ]}, goto next title at one higher level. if (checkMode(VimMode::Normal)) { processTitleJump(m_tokens, true, -1); } } break; } break; } case Qt::Key_AsciiTilde: { if (modifiers == Qt::ShiftModifier) { tryGetRepeatToken(m_keys, m_tokens); if (hasActionToken() || !m_keys.isEmpty()) { break; } // Reverse the case. addActionToken(Action::ReverseCase); processCommand(m_tokens); break; } 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; } clear_accept: resetState(); amendCursorPosition(); if (m_insertModeAfterCommand && !checkMode(VimMode::Visual) && !checkMode(VimMode::VisualLine)) { m_insertModeAfterCommand = false; setMode(VimMode::Insert); } m_editor->makeBlockVisible(m_editor->textCursorW().block()); accept: ret = true; // Only alter the autoIndentPos when the key is handled by Vim. if (p_autoIndentPos) { *p_autoIndentPos = autoIndentPos; } exit: m_resetPositionInBlock = resetPositionInBlock; emit vimStatusUpdated(this); return ret; } void VVim::resetState() { m_keys.clear(); m_tokens.clear(); m_pendingKeys.clear(); setCurrentRegisterName(c_unnamedRegister); m_resetPositionInBlock = true; m_registerPending = false; } VimMode VVim::getMode() const { return m_mode; } void VVim::setMode(VimMode p_mode, bool p_clearSelection, int p_position) { if (m_mode != p_mode) { QTextCursor cursor = m_editor->textCursorW(); int position = p_position; if (position == -1) { if (m_mode == VimMode::Visual && p_mode == VimMode::Normal && cursor.position() > cursor.anchor()) { position = cursor.position() - 1; } else if (m_mode == VimMode::Insert && p_mode == VimMode::Normal && !cursor.atBlockStart()) { position = cursor.position() - 1; if (position < 0) { position = 0; } } } if (p_clearSelection) { clearSelection(); } if (p_mode == VimMode::Insert) { m_editor->setInputMethodEnabled(true); } else if (g_config->getEnableSmartImInVimMode()) { m_editor->setInputMethodEnabled(false); } m_mode = p_mode; resetState(); VMdEditor *mdEditor = dynamic_cast(m_editor); switch (m_mode) { case VimMode::Insert: setCursorBlockMode(m_editor, CursorBlock::None); if (mdEditor) { mdEditor->setHighlightCursorLineBlockEnabled(false); } break; case VimMode::Visual: m_positionBeforeVisualMode = cursor.anchor(); V_FALLTHROUGH; default: setCursorBlockMode(m_editor, CursorBlock::RightSide); if (mdEditor && g_config->getHighlightCursorLine()) { QString color; if (m_mode == VimMode::Normal) { color = g_config->getEditorVimNormalBg(); } else { color = g_config->getEditorVimVisualBg(); } mdEditor->setCursorLineBlockBg(color); mdEditor->setHighlightCursorLineBlockEnabled(true); } break; } if (position != -1) { cursor.setPosition(position); m_editor->setTextCursorW(cursor); } amendCursorPosition(); emit modeChanged(m_mode); emit vimStatusUpdated(this); } } void VVim::processCommand(QList &p_tokens) { if (p_tokens.isEmpty()) { return; } V_ASSERT(p_tokens.at(0).isAction()); Token act = p_tokens.takeFirst(); switch (act.m_action) { case Action::Move: processMoveAction(p_tokens); break; case Action::Delete: processDeleteAction(p_tokens); break; case Action::Copy: processCopyAction(p_tokens); break; case Action::Paste: processPasteAction(p_tokens, false); break; case Action::PasteBefore: processPasteAction(p_tokens, true); break; case Action::Change: processChangeAction(p_tokens); break; case Action::Indent: processIndentAction(p_tokens, IndentType::Indent); break; case Action::UnIndent: processIndentAction(p_tokens, IndentType::UnIndent); break; case Action::AutoIndent: processIndentAction(p_tokens, IndentType::AutoIndent); break; case Action::ToLower: processToLowerAction(p_tokens, true); break; case Action::ToUpper: processToLowerAction(p_tokens, false); break; case Action::Undo: processUndoAction(p_tokens); break; case Action::Redo: processRedoAction(p_tokens); break; case Action::RedrawAtTop: processRedrawLineAction(p_tokens, 0); break; case Action::RedrawAtCenter: processRedrawLineAction(p_tokens, 1); break; case Action::RedrawAtBottom: processRedrawLineAction(p_tokens, 2); break; case Action::JumpPreviousLocation: processJumpLocationAction(p_tokens, false); break; case Action::JumpNextLocation: processJumpLocationAction(p_tokens, true); break; case Action::Replace: processReplaceAction(p_tokens); break; case Action::ReverseCase: processReverseCaseAction(p_tokens); break; case Action::Join: processJoinAction(p_tokens, true); break; case Action::JoinNoModification: processJoinAction(p_tokens, false); break; default: p_tokens.clear(); break; } Q_ASSERT(p_tokens.isEmpty()); } int VVim::numberFromKeySequence(const QList &p_keys) { int num = 0; for (auto const & key : p_keys) { if (key.isDigit()) { num = num * 10 + key.toDigit(); } else { return -1; } } return num == 0 ? -1 : num; } bool VVim::tryGetRepeatToken(QList &p_keys, QList &p_tokens) { if (!p_keys.isEmpty()) { int repeat = numberFromKeySequence(p_keys); if (repeat != -1) { p_tokens.append(Token(repeat)); p_keys.clear(); return true; } } return false; } void VVim::processMoveAction(QList &p_tokens) { // Only moving left/right could change this. static int positionInBlock = 0; Token to = p_tokens.takeFirst(); V_ASSERT(to.isRepeat() || to.isMovement()); Token mvToken; int repeat = -1; if (to.isRepeat()) { repeat = to.m_repeat; mvToken = p_tokens.takeFirst(); } else { mvToken = to; } if (!mvToken.isMovement() || !p_tokens.isEmpty()) { p_tokens.clear(); return; } QTextCursor cursor = m_editor->textCursorW(); if (m_resetPositionInBlock) { positionInBlock = cursor.positionInBlock(); } QTextCursor::MoveMode moveMode = (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor; bool hasMoved = processMovement(cursor, moveMode, mvToken, repeat); if (hasMoved) { // Maintain positionInBlock. switch (mvToken.m_movement) { case Movement::Left: case Movement::Right: positionInBlock = cursor.positionInBlock(); break; case Movement::Up: case Movement::Down: case Movement::PageUp: case Movement::PageDown: case Movement::HalfPageUp: case Movement::HalfPageDown: setCursorPositionInBlock(cursor, positionInBlock, moveMode); break; default: break; } if (checkMode(VimMode::VisualLine)) { expandSelectionToWholeLines(cursor); } else if (checkMode(VimMode::Visual)) { maintainSelectionInVisualMode(&cursor); } m_editor->setTextCursorW(cursor); } } bool VVim::processMovement(QTextCursor &p_cursor, QTextCursor::MoveMode p_moveMode, const Token &p_token, int p_repeat) { V_ASSERT(p_token.isMovement()); bool hasMoved = false; bool inclusive = true; bool forward = true; QTextDocument *doc = p_cursor.document(); switch (p_token.m_movement) { case Movement::Left: { if (p_repeat == -1) { p_repeat = 1; } int pib = p_cursor.positionInBlock(); p_repeat = qMin(pib, p_repeat); if (p_repeat > 0) { p_cursor.movePosition(QTextCursor::Left, p_moveMode, p_repeat); hasMoved = true; } break; } case Movement::Right: { if (p_repeat == -1) { p_repeat = 1; } if (checkMode(VimMode::Visual)) { int pos = p_cursor.position(); if (pos == p_cursor.anchor() - 1 && pos == m_positionBeforeVisualMode) { ++p_repeat; } } int pib = p_cursor.positionInBlock(); int length = p_cursor.block().length(); if (length - pib <= p_repeat) { p_repeat = length - pib - 1; } if (p_repeat > 0) { p_cursor.movePosition(QTextCursor::Right, p_moveMode, p_repeat); hasMoved = true; } break; } case Movement::Up: { if (p_repeat == -1) { p_repeat = 1; } p_repeat = qMin(p_cursor.block().blockNumber(), p_repeat); if (p_repeat > 0) { p_cursor.movePosition(QTextCursor::PreviousBlock, p_moveMode, p_repeat); hasMoved = true; } break; } case Movement::Down: { if (p_repeat == -1) { p_repeat = 1; } int blockCount = doc->blockCount(); p_repeat = qMin(blockCount - 1 - p_cursor.block().blockNumber(), p_repeat); if (p_repeat > 0) { p_cursor.movePosition(QTextCursor::NextBlock, p_moveMode, p_repeat); hasMoved = true; } break; } case Movement::VisualUp: { if (p_repeat == -1) { p_repeat = 1; } p_cursor.movePosition(QTextCursor::Up, p_moveMode, p_repeat); hasMoved = true; break; } case Movement::VisualDown: { if (p_repeat == -1) { p_repeat = 1; } p_cursor.movePosition(QTextCursor::Down, p_moveMode, p_repeat); hasMoved = true; break; } case Movement::PageUp: { if (p_repeat == -1) { p_repeat = 1; } int blockStep = blockCountOfPageStep() * p_repeat; int block = p_cursor.block().blockNumber(); block = qMax(0, block - blockStep); p_cursor.setPosition(doc->findBlockByNumber(block).position(), p_moveMode); hasMoved = true; break; } case Movement::PageDown: { if (p_repeat == -1) { p_repeat = 1; } int blockStep = blockCountOfPageStep() * p_repeat; int block = p_cursor.block().blockNumber(); block = qMin(block + blockStep, doc->blockCount() - 1); p_cursor.setPosition(doc->findBlockByNumber(block).position(), p_moveMode); hasMoved = true; break; } case Movement::HalfPageUp: { if (p_repeat == -1) { p_repeat = 1; } int blockStep = blockCountOfPageStep(); int halfBlockStep = qMax(blockStep / 2, 1); blockStep = p_repeat * halfBlockStep; int block = p_cursor.block().blockNumber(); block = qMax(0, block - blockStep); p_cursor.setPosition(doc->findBlockByNumber(block).position(), p_moveMode); hasMoved = true; break; } case Movement::HalfPageDown: { if (p_repeat == -1) { p_repeat = 1; } int blockStep = blockCountOfPageStep(); int halfBlockStep = qMax(blockStep / 2, 1); blockStep = p_repeat * halfBlockStep; int block = p_cursor.block().blockNumber(); block = qMin(block + blockStep, doc->blockCount() - 1); p_cursor.setPosition(doc->findBlockByNumber(block).position(), p_moveMode); hasMoved = true; break; } case Movement::StartOfLine: { Q_ASSERT(p_repeat == -1); // Start of the Line (block). if (!p_cursor.atBlockStart()) { p_cursor.movePosition(QTextCursor::StartOfBlock, p_moveMode, 1); hasMoved = true; } break; } case Movement::StartOfVisualLine: { // Start of the visual line. if (!p_cursor.atBlockStart()) { p_cursor.movePosition(QTextCursor::StartOfLine, p_moveMode, 1); hasMoved = true; } break; } case Movement::EndOfLine: { // End of line (block). if (p_repeat == -1) { p_repeat = 1; } else if (p_repeat > 1) { // Move down (p_repeat-1) blocks. p_cursor.movePosition(QTextCursor::NextBlock, p_moveMode, p_repeat - 1); } // Move to the end of block. p_cursor.movePosition(QTextCursor::EndOfBlock, p_moveMode, 1); hasMoved = true; break; } case Movement::FirstCharacter: { // p_repeat is not considered in this command. // If all the block is space, just move to the end of block; otherwise, // move to the first non-space character. VEditUtils::moveCursorFirstNonSpaceCharacter(p_cursor, p_moveMode); if (!p_cursor.atEnd() && useLeftSideOfCursor(p_cursor)) { // Move one character forward. p_cursor.movePosition(QTextCursor::NextCharacter, p_moveMode); } hasMoved = true; break; } case Movement::LineJump: { // Jump to the first non-space character of @p_repeat line (block). V_ASSERT(p_repeat > 0); // Record current location. m_locations.addLocation(p_cursor); // @p_repeat starts from 1 while block number starts from 0. QTextBlock block = doc->findBlockByNumber(p_repeat - 1); if (block.isValid()) { p_cursor.setPosition(block.position(), p_moveMode); } else { // Go beyond the document. p_cursor.movePosition(QTextCursor::End, p_moveMode, 1); } VEditUtils::moveCursorFirstNonSpaceCharacter(p_cursor, p_moveMode); if (!p_cursor.atEnd() && useLeftSideOfCursor(p_cursor)) { // Move one character forward. p_cursor.movePosition(QTextCursor::NextCharacter, p_moveMode); } hasMoved = true; break; } case Movement::StartOfDocument: { // Jump to the first non-space character of the start of the document. V_ASSERT(p_repeat == -1); // Record current location. m_locations.addLocation(p_cursor); p_cursor.movePosition(QTextCursor::Start, p_moveMode, 1); VEditUtils::moveCursorFirstNonSpaceCharacter(p_cursor, p_moveMode); if (!p_cursor.atEnd() && useLeftSideOfCursor(p_cursor)) { // Move one character forward. p_cursor.movePosition(QTextCursor::NextCharacter, p_moveMode); } hasMoved = true; break; } case Movement::EndOfDocument: { // Jump to the first non-space character of the end of the document. V_ASSERT(p_repeat == -1); // Record current location. m_locations.addLocation(p_cursor); p_cursor.movePosition(QTextCursor::End, p_moveMode, 1); VEditUtils::moveCursorFirstNonSpaceCharacter(p_cursor, p_moveMode); if (!p_cursor.atEnd() && useLeftSideOfCursor(p_cursor)) { // Move one character forward. p_cursor.movePosition(QTextCursor::NextCharacter, p_moveMode); } hasMoved = true; break; } case Movement::WordForward: { // Go to the start of next word. if (p_repeat == -1) { p_repeat = 1; } while (p_repeat) { if (p_cursor.atEnd()) { break; } p_cursor.movePosition(QTextCursor::NextWord, p_moveMode); if (p_cursor.atBlockEnd()) { // dw/yw/cw will stop at the end of the line. if (p_repeat == 1 && checkMode(VimMode::Normal) && p_moveMode == QTextCursor::KeepAnchor) { --p_repeat; } continue; } else if (doc->characterAt(p_cursor.position()).isSpace() || VEditUtils::isSpaceBlock(p_cursor.block())) { continue; } --p_repeat; } if (!p_cursor.atEnd() && useLeftSideOfCursor(p_cursor)) { // Move one character forward. p_cursor.movePosition(QTextCursor::NextCharacter, p_moveMode); } hasMoved = true; break; } case Movement::WORDForward: { // Go to the start of next WORD. if (p_repeat == -1) { p_repeat = 1; } while (p_repeat) { if (p_cursor.atEnd()) { break; } int start, end; // [start, end] is current WORD. VEditUtils::findCurrentWORD(p_cursor, start, end); // Move cursor to end of current WORD. p_cursor.setPosition(end, p_moveMode); if (p_repeat == 1 && checkMode(VimMode::Normal) && p_moveMode == QTextCursor::KeepAnchor) { // dW/yW/cW will stop at the end of the line. moveCursorAcrossSpaces(p_cursor, p_moveMode, true, true); if (p_cursor.atBlockEnd()) { --p_repeat; continue; } } // Skip spaces. moveCursorAcrossSpaces(p_cursor, p_moveMode, true); if (p_cursor.atBlockEnd()) { continue; } --p_repeat; } if (!p_cursor.atEnd() && useLeftSideOfCursor(p_cursor)) { // Move one character forward. p_cursor.movePosition(QTextCursor::NextCharacter, p_moveMode); } hasMoved = true; break; } case Movement::ForwardEndOfWord: { // Go to the end of current word or next word. if (p_repeat == -1) { p_repeat = 1; } int pos = p_cursor.position(); bool leftSideBefore = useLeftSideOfCursor(p_cursor); // First move to the end of current word. p_cursor.movePosition(QTextCursor::EndOfWord, p_moveMode, 1); if (p_cursor.position() > pos) { if (p_cursor.position() > pos + 1 || (leftSideBefore && useLeftSideOfCursor(p_cursor))) { // We did move. p_repeat -= 1; } } while (p_repeat) { if (p_cursor.atEnd()) { break; } pos = p_cursor.position(); p_cursor.movePosition(QTextCursor::EndOfWord, p_moveMode); if (p_cursor.position() == pos) { // Need to move to the start of next word. p_cursor.movePosition(QTextCursor::NextWord, p_moveMode); if (p_cursor.atBlockEnd()) { continue; } p_cursor.movePosition(QTextCursor::EndOfWord, p_moveMode); if (p_cursor.atBlockStart()) { continue; } } --p_repeat; } // Move one character back. if (!p_cursor.atBlockStart()) { if (p_moveMode == QTextCursor::MoveAnchor || (checkMode(VimMode::Visual) && !useLeftSideOfCursor(p_cursor))) { p_cursor.movePosition(QTextCursor::PreviousCharacter, p_moveMode); } } hasMoved = true; break; } case Movement::ForwardEndOfWORD: { // Go to the end of current WORD or next WORD. if (p_repeat == -1) { p_repeat = 1; } int pos = p_cursor.position(); bool leftSideBefore = useLeftSideOfCursor(p_cursor); while (p_repeat) { if (p_cursor.atEnd()) { break; } // Skip spaces. moveCursorAcrossSpaces(p_cursor, p_moveMode, true); if (p_cursor.atBlockEnd()) { continue; } int start, end; // [start, end] is current WORD. VEditUtils::findCurrentWORD(p_cursor, start, end); // Move cursor to the end of current WORD. p_cursor.setPosition(end, p_moveMode); if (p_cursor.position() > pos + 1 || (leftSideBefore && useLeftSideOfCursor(p_cursor))) { --p_repeat; } } // Move one character back. if (!p_cursor.atBlockStart()) { if (p_moveMode == QTextCursor::MoveAnchor || (checkMode(VimMode::Visual) && !useLeftSideOfCursor(p_cursor))) { p_cursor.movePosition(QTextCursor::PreviousCharacter, p_moveMode); } } hasMoved = true; break; } case Movement::WordBackward: { // Go to the start of previous word or current word. if (p_repeat == -1) { p_repeat = 1; } int pos = p_cursor.position(); // first move to the start of current word. p_cursor.movePosition(QTextCursor::StartOfWord, p_moveMode); if (p_cursor.position() == pos && useLeftSideOfCursor(p_cursor)) { // Cursor did not move and now is at the start of a word. // Actually we are using the left side character so we need to move // to previous word. p_cursor.movePosition(QTextCursor::PreviousWord, p_moveMode); } if (p_cursor.position() < pos) { if (p_cursor.position() < pos - 1 || !useLeftSideOfCursor(p_cursor)) { // We did move. p_repeat -= 1; } } while (p_repeat) { if (p_cursor.atStart()) { break; } pos = p_cursor.position(); p_cursor.movePosition(QTextCursor::StartOfWord, p_moveMode); if (p_cursor.position() == pos) { // Need to move to the start of previous word. p_cursor.movePosition(QTextCursor::PreviousWord, p_moveMode); if (p_cursor.atBlockEnd()) { continue; } if (p_cursor.atBlockStart() && doc->characterAt(p_cursor.position()).isSpace()) { continue; } } --p_repeat; } if (!p_cursor.atEnd() && useLeftSideOfCursor(p_cursor)) { // Move one character forward. p_cursor.movePosition(QTextCursor::NextCharacter, p_moveMode); } hasMoved = true; break; } case Movement::WORDBackward: { // Go to the start of previous WORD or current WORD. if (p_repeat == -1) { p_repeat = 1; } int pos = p_cursor.position(); while (p_repeat) { if (p_cursor.atStart()) { break; } // Skip Spaces. moveCursorAcrossSpaces(p_cursor, p_moveMode, false); if (p_cursor.atBlockStart()) { continue; } p_cursor.movePosition(QTextCursor::PreviousCharacter, p_moveMode); int start, end; // [start, end] is current WORD. VEditUtils::findCurrentWORD(p_cursor, start, end); // Move cursor to the start of current WORD. p_cursor.setPosition(start, p_moveMode); if (p_cursor.position() < pos - 1 || !useLeftSideOfCursor(p_cursor)) { --p_repeat; } } if (!p_cursor.atEnd() && useLeftSideOfCursor(p_cursor)) { // Move one character forward. p_cursor.movePosition(QTextCursor::NextCharacter, p_moveMode); } hasMoved = true; break; } case Movement::BackwardEndOfWord: { // Go to the end of previous word. if (p_repeat == -1) { p_repeat = 1; } if (useLeftSideOfCursor(p_cursor)) { // Move one previous char. p_cursor.movePosition(QTextCursor::PreviousCharacter, p_moveMode); } int pos = p_cursor.position(); // Move across spaces backward. moveCursorAcrossSpaces(p_cursor, p_moveMode, false); int start, end; VEditUtils::findCurrentWord(p_cursor, start, end); if (pos != p_cursor.position() || start == end || start == pos) { // We are alreay at the end of previous word. --p_repeat; // Move it to the start of current word. p_cursor.movePosition(QTextCursor::PreviousWord, p_moveMode); } else { p_cursor.movePosition(QTextCursor::StartOfWord, p_moveMode); } while (p_repeat) { if (p_cursor.atStart()) { break; } p_cursor.movePosition(QTextCursor::PreviousWord, p_moveMode); if (p_cursor.atBlockEnd()) { continue; } if (p_cursor.atBlockStart() && doc->characterAt(p_cursor.position()).isSpace()) { continue; } --p_repeat; } // Move it to the end. p_cursor.movePosition(QTextCursor::EndOfWord, p_moveMode); // Move one character back. if (!p_cursor.atBlockStart()) { if (p_moveMode == QTextCursor::MoveAnchor || (checkMode(VimMode::Visual) && !useLeftSideOfCursor(p_cursor)) || p_cursor.position() <= p_cursor.anchor()) { p_cursor.movePosition(QTextCursor::PreviousCharacter, p_moveMode); } } hasMoved = true; break; } case Movement::BackwardEndOfWORD: { // Go to the end of previous WORD. if (p_repeat == -1) { p_repeat = 1; } if (useLeftSideOfCursor(p_cursor)) { // Move one previous char. p_cursor.movePosition(QTextCursor::PreviousCharacter, p_moveMode); } int pos = p_cursor.position(); // Move across spaces backward. moveCursorAcrossSpaces(p_cursor, p_moveMode, false); int start, end; VEditUtils::findCurrentWORD(p_cursor, start, end); if (pos != p_cursor.position() || start == end) { // We are alreay at the end of previous WORD. --p_repeat; // Move it to the start of current WORD. p_cursor.setPosition(start, p_moveMode); } else { // Move it to the start of current WORD. p_cursor.setPosition(start, p_moveMode); } while (p_repeat) { if (p_cursor.atStart()) { break; } moveCursorAcrossSpaces(p_cursor, p_moveMode, false); if (p_cursor.atBlockStart()) { continue; } if (p_cursor.atBlockStart() && doc->characterAt(p_cursor.position()).isSpace()) { continue; } VEditUtils::findCurrentWORD(p_cursor, start, end); p_cursor.setPosition(start, p_moveMode); --p_repeat; } // Move it to the end. VEditUtils::findCurrentWORD(p_cursor, start, end); p_cursor.setPosition(end, p_moveMode); // Move one character back. if (!p_cursor.atBlockStart()) { if (p_moveMode == QTextCursor::MoveAnchor || (checkMode(VimMode::Visual) && !useLeftSideOfCursor(p_cursor)) || p_cursor.position() <= p_cursor.anchor()) { p_cursor.movePosition(QTextCursor::PreviousCharacter, p_moveMode); } } hasMoved = true; 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: if (p_repeat == -1) { p_repeat = 1; } 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, useLeftSideOfCursor(p_cursor), p_repeat); } break; } case Movement::MarkJump: // Fall through. case Movement::MarkJumpLine: { // repeat is useless here. const Key &key = p_token.m_key; QChar target = keyToChar(key.m_key, key.m_modifiers); Location loc = m_marks.getMarkLocation(target); if (loc.isValid()) { if (loc.m_blockNumber >= doc->blockCount()) { // Invalid block number. message(tr("Mark not set")); m_marks.clearMark(target); break; } // Different from Vim: // We just use the block number for mark, so if we delete the line // where the mark locates, we could not detect if it is set or not. QTextBlock block = doc->findBlockByNumber(loc.m_blockNumber); p_cursor.setPosition(block.position(), p_moveMode); if (p_token.m_movement == Movement::MarkJump) { setCursorPositionInBlock(p_cursor, loc.m_positionInBlock, p_moveMode); } else { // Jump to the first non-space character. VEditUtils::moveCursorFirstNonSpaceCharacter(p_cursor, p_moveMode); } if (!p_cursor.atEnd() && useLeftSideOfCursor(p_cursor)) { // Move one character forward. p_cursor.movePosition(QTextCursor::NextCharacter, p_moveMode); } hasMoved = true; } break; } case Movement::FindPair: { Q_ASSERT(p_repeat == -1); int anchor = p_cursor.anchor(); int position = p_cursor.position(); QList> pairs; pairs.append(QPair('(', ')')); pairs.append(QPair('[', ']')); pairs.append(QPair('{', '}')); // Find forward for a pair (), [], and {}. QList targets; for (auto const & pair : pairs) { targets.append(pair.first); targets.append(pair.second); } // First check if current char hits the targets. bool useLeftSideBefore = useLeftSideOfCursor(p_cursor); QChar ch = doc->characterAt(useLeftSideBefore ? position - 1 : position); int idx = targets.indexOf(ch); if (idx == -1) { idx = VEditUtils::findTargetsWithinBlock(p_cursor, targets, true, useLeftSideOfCursor(p_cursor), true); } else if (useLeftSideBefore) { // Move one character back to let p_cursor position at the pair. p_cursor.movePosition(QTextCursor::PreviousCharacter, p_moveMode); } if (idx == -1) { break; } idx /= 2; int pairPosition = p_cursor.position(); bool ret = VEditUtils::selectPairTargetAround(p_cursor, pairs.at(idx).first, pairs.at(idx).second, true, true, 1); if (ret) { // Found matched pair. int first = p_cursor.position(); int second = p_cursor.anchor(); if (first > second) { int tmp = first; first = second; second = tmp; } if (!(checkMode(VimMode::Normal) && p_moveMode == QTextCursor::KeepAnchor)) { --second; } int target = first; if (first == pairPosition) { target = second; } if (anchor > target && !p_cursor.atEnd() && !useLeftSideOfCursor(p_cursor)) { ++anchor; } p_cursor.setPosition(anchor); p_cursor.setPosition(target, p_moveMode); if (!p_cursor.atEnd() && useLeftSideOfCursor(p_cursor)) { // Move one character forward. p_cursor.movePosition(QTextCursor::NextCharacter, p_moveMode); } hasMoved = true; break; } else { // Restore the cursor position. p_cursor.setPosition(anchor); if (anchor != position) { p_cursor.setPosition(position, QTextCursor::KeepAnchor); } } 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); bool useLeftSideBefore = useLeftSideOfCursor(p_cursor); const SearchItem &item = m_searchHistory.lastItem(); while (--p_repeat >= 0) { bool found = m_editor->findText(item.m_text, item.m_options, forward ? item.m_forward : !item.m_forward, &p_cursor, p_moveMode, useLeftSideBefore); if (found) { hasMoved = true; useLeftSideBefore = false; } else { break; } } if (hasMoved) { if (!p_cursor.atEnd() && useLeftSideOfCursor(p_cursor)) { p_cursor.movePosition(QTextCursor::NextCharacter, 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; VEditUtils::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()) { VEditUtils::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); if (!p_cursor.atEnd() && useLeftSideOfCursor(p_cursor)) { p_cursor.movePosition(QTextCursor::NextCharacter, p_moveMode); } break; } case Movement::ParagraphUp: forward = false; // Fall through. case Movement::ParagraphDown: { if (p_repeat == -1) { p_repeat = 1; } // Record current location. m_locations.addLocation(p_cursor); int oriPos = p_cursor.position(); int position = VEditUtils::findNextEmptyBlock(p_cursor, forward, p_repeat); if (position == -1) { // No empty block. Move to the first/last character. p_cursor.movePosition(forward ? QTextCursor::End : QTextCursor::Start, p_moveMode); hasMoved = p_cursor.position() != oriPos; } else { p_cursor.setPosition(position, p_moveMode); hasMoved = true; } break; } default: break; } return hasMoved; } bool VVim::selectRange(QTextCursor &p_cursor, const QTextDocument *p_doc, Range p_range, int p_repeat) { bool hasMoved = false; QTextCursor::MoveMode moveMode = QTextCursor::KeepAnchor; bool around = false; QChar opening; QChar closing; bool crossBlock = false; bool multipleTargets = false; Q_UNUSED(p_doc); switch (p_range) { case Range::Line: { // Visual mode, just select selected lines. if (checkMode(VimMode::Visual) || checkMode(VimMode::VisualLine)) { expandSelectionToWholeLines(p_cursor); hasMoved = true; break; } // Current line and next (p_repeat - 1) lines. if (p_repeat == -1) { p_repeat = 1; } if (p_repeat > 1) { p_cursor.movePosition(QTextCursor::NextBlock, moveMode, p_repeat - 1); } expandSelectionToWholeLines(p_cursor); hasMoved = true; break; } case Range::WordAround: around = true; // Fall through. case Range::WordInner: { Q_ASSERT(p_repeat == -1); bool spaces = false; int start, end; VEditUtils::findCurrentWord(p_cursor, start, end); if (start == end) { // Select the space between previous word and next word. findCurrentSpace(p_cursor, start, end); spaces = true; } if (start != end) { p_cursor.setPosition(start, QTextCursor::MoveAnchor); p_cursor.setPosition(end, moveMode); hasMoved = true; if (around) { if (spaces) { // Select the word by the end of spaces. if (!p_cursor.atBlockEnd()) { p_cursor.movePosition(QTextCursor::EndOfWord, moveMode); } } else { // Select additional spaces at two ends. expandSelectionAcrossSpacesWithinBlock(p_cursor); } } } break; } case Range::WORDAround: around = true; // Fall through. case Range::WORDInner: { Q_ASSERT(p_repeat == -1); bool spaces = false; int start, end; findCurrentSpace(p_cursor, start, end); if (start == end) { VEditUtils::findCurrentWORD(p_cursor, start, end); } else { // Select the space between previous WORD and next WORD. spaces = true; } if (start != end) { p_cursor.setPosition(start, QTextCursor::MoveAnchor); p_cursor.setPosition(end, moveMode); hasMoved = true; if (around) { if (spaces) { // Select the WORD by the end of spaces. if (!p_cursor.atBlockEnd()) { // Skip spaces (mainly across block). moveCursorAcrossSpaces(p_cursor, moveMode, true); // [start, end] is current WORD. VEditUtils::findCurrentWORD(p_cursor, start, end); // Move cursor to the end of current WORD. p_cursor.setPosition(end, moveMode); } } else { // Select additional spaces at two ends. expandSelectionAcrossSpacesWithinBlock(p_cursor); } } } break; } case Range::ParenthesisAround: { around = true; opening = '('; closing = ')'; crossBlock = true; multipleTargets = true; goto handlePairTarget; } case Range::ParenthesisInner: { around = false; opening = '('; closing = ')'; crossBlock = true; multipleTargets = true; goto handlePairTarget; } case Range::BracketAround: { around = true; opening = '['; closing = ']'; crossBlock = true; multipleTargets = true; goto handlePairTarget; } case Range::BracketInner: { around = false; opening = '['; closing = ']'; crossBlock = true; multipleTargets = true; goto handlePairTarget; } case Range::AngleBracketAround: { around = true; opening = '<'; closing = '>'; crossBlock = true; multipleTargets = true; goto handlePairTarget; } case Range::AngleBracketInner: { around = false; opening = '<'; closing = '>'; crossBlock = true; multipleTargets = true; goto handlePairTarget; } case Range::BraceAround: { around = true; opening = '{'; closing = '}'; crossBlock = true; multipleTargets = true; goto handlePairTarget; } case Range::BraceInner: { around = false; opening = '{'; closing = '}'; crossBlock = true; multipleTargets = true; goto handlePairTarget; } case Range::DoubleQuoteAround: { around = true; opening = '"'; closing = '"'; crossBlock = false; multipleTargets = false; goto handlePairTarget; } case Range::DoubleQuoteInner: { around = false; opening = '"'; closing = '"'; crossBlock = false; multipleTargets = false; goto handlePairTarget; } case Range::BackQuoteAround: { around = true; opening = '`'; closing = '`'; crossBlock = false; multipleTargets = false; goto handlePairTarget; } case Range::BackQuoteInner: { around = false; opening = '`'; closing = '`'; crossBlock = false; multipleTargets = false; goto handlePairTarget; } case Range::QuoteAround: { around = true; opening = '\''; closing = '\''; crossBlock = false; multipleTargets = false; goto handlePairTarget; } case Range::QuoteInner: { around = false; opening = '\''; closing = '\''; crossBlock = false; multipleTargets = false; handlePairTarget: if (p_repeat == -1) { p_repeat = 1; } else if (p_repeat > 1 && !multipleTargets) { // According to the behavior of Vim. p_repeat = 1; around = true; } hasMoved = VEditUtils::selectPairTargetAround(p_cursor, opening, closing, around, crossBlock, p_repeat); break; } default: break; } return hasMoved; } void VVim::processDeleteAction(QList &p_tokens) { Token to = p_tokens.takeFirst(); int repeat = -1; if (to.isRepeat()) { repeat = to.m_repeat; to = p_tokens.takeFirst(); } if ((!to.isMovement() && !to.isRange()) || !p_tokens.isEmpty()) { p_tokens.clear(); return; } QTextCursor cursor = m_editor->textCursorW(); QTextDocument *doc = m_editor->documentW(); bool hasMoved = false; QTextCursor::MoveMode moveMode = QTextCursor::KeepAnchor; if (to.isRange()) { cursor.beginEditBlock(); hasMoved = selectRange(cursor, doc, to.m_range, repeat); if (hasMoved) { // Whether the range may cross blocks. bool mayCrossBlock = false; switch (to.m_range) { case Range::Line: { // dd, delete current line. if (cursor.hasSelection()) { repeat = VEditUtils::selectedBlockCount(cursor); deleteSelectedText(cursor, true); } else { VEditUtils::removeBlock(cursor); saveToRegister("\n"); repeat = 1; } message(tr("%1 fewer %2").arg(repeat).arg(repeat > 1 ? tr("lines") : tr("line"))); break; } case Range::ParenthesisInner: // Fall through. case Range::ParenthesisAround: // Fall through. case Range::BracketInner: // Fall through. case Range::BracketAround: // Fall through. case Range::AngleBracketInner: // Fall through. case Range::AngleBracketAround: // Fall through. case Range::BraceInner: // Fall through. case Range::BraceAround: // Fall through. mayCrossBlock = true; V_FALLTHROUGH; case Range::WordAround: // Fall through. case Range::WordInner: // Fall through. case Range::WORDAround: // Fall through. case Range::WORDInner: // Fall through. case Range::QuoteInner: // Fall through. case Range::QuoteAround: // Fall through. case Range::DoubleQuoteInner: // Fall through. case Range::DoubleQuoteAround: // Fall through. case Range::BackQuoteInner: // Fall through. case Range::BackQuoteAround: { if (cursor.hasSelection()) { bool clearEmptyBlock = false; if (mayCrossBlock && VEditUtils::selectedBlockCount(cursor) > 1) { clearEmptyBlock = true; } int blockCount = 0; if (clearEmptyBlock) { blockCount = doc->blockCount(); } deleteSelectedText(cursor, clearEmptyBlock); if (clearEmptyBlock) { int nrBlock = blockCount - doc->blockCount(); Q_ASSERT(nrBlock > 0); message(tr("%1 fewer %2").arg(nrBlock).arg(nrBlock > 1 ? tr("lines") : tr("line"))); } } break; } default: return; } } cursor.endEditBlock(); goto exit; } V_ASSERT(to.isMovement()); // Filter out not supported movement for DELETE action. switch (to.m_movement) { case Movement::PageUp: case Movement::PageDown: case Movement::HalfPageUp: case Movement::HalfPageDown: return; default: break; } cursor.beginEditBlock(); if (checkMode(VimMode::VisualLine) || checkMode(VimMode::Visual)) { // Visual mode, omitting repeat and movement. // Different from Vim: // If there is no selection in Visual mode, we do nothing. if (cursor.hasSelection()) { hasMoved = true; deleteSelectedText(cursor, m_mode == VimMode::VisualLine); } else if (checkMode(VimMode::Visual)) { hasMoved = true; VEditUtils::removeBlock(cursor); saveToRegister("\n"); } cursor.endEditBlock(); goto exit; } hasMoved = processMovement(cursor, moveMode, to, repeat); if (repeat == -1) { repeat = 1; } if (hasMoved) { bool clearEmptyBlock = false; switch (to.m_movement) { case Movement::Up: { expandSelectionToWholeLines(cursor); clearEmptyBlock = true; qDebug() << "delete up" << repeat << "lines"; break; } case Movement::Down: { expandSelectionToWholeLines(cursor); clearEmptyBlock = true; qDebug() << "delete down" << repeat << "lines"; break; } case Movement::EndOfLine: { // End of line (block). if (repeat > 1) { clearEmptyBlock = true; } qDebug() << "delete till end of" << repeat << "line"; break; } case Movement::LineJump: { expandSelectionToWholeLines(cursor); clearEmptyBlock = true; qDebug() << "delete till line" << repeat; break; } case Movement::StartOfDocument: { expandSelectionToWholeLines(cursor); clearEmptyBlock = true; qDebug() << "delete till start of document"; break; } case Movement::EndOfDocument: { expandSelectionToWholeLines(cursor); clearEmptyBlock = true; qDebug() << "delete till end of document"; break; } // ParagraphUp and ParagraphDown are a little different from Vim in // block deletion. default: break; } if (clearEmptyBlock) { int nrBlock = VEditUtils::selectedBlockCount(cursor); message(tr("%1 fewer %2").arg(nrBlock).arg(nrBlock > 1 ? tr("lines") : tr("line"))); } deleteSelectedText(cursor, clearEmptyBlock); } cursor.endEditBlock(); exit: if (hasMoved) { m_editor->setTextCursorW(cursor); } } void VVim::processCopyAction(QList &p_tokens) { Token to = p_tokens.takeFirst(); int repeat = -1; if (to.isRepeat()) { repeat = to.m_repeat; to = p_tokens.takeFirst(); } if ((!to.isMovement() && !to.isRange()) || !p_tokens.isEmpty()) { p_tokens.clear(); return; } QTextCursor cursor = m_editor->textCursorW(); QTextDocument *doc = m_editor->documentW(); int oriPos = cursor.position(); bool changed = false; QTextCursor::MoveMode moveMode = QTextCursor::KeepAnchor; if (to.isRange()) { cursor.beginEditBlock(); changed = selectRange(cursor, doc, to.m_range, repeat); if (changed) { // Whether the range may cross blocks. bool mayCrossBlock = false; switch (to.m_range) { case Range::Line: { // yy, copy current line. if (cursor.hasSelection()) { repeat = VEditUtils::selectedBlockCount(cursor); copySelectedText(cursor, true); } else { saveToRegister("\n"); repeat = 1; } message(tr("%1 %2 yanked").arg(repeat).arg(repeat > 1 ? tr("lines") : tr("line"))); break; } case Range::ParenthesisInner: // Fall through. case Range::ParenthesisAround: // Fall through. case Range::BracketInner: // Fall through. case Range::BracketAround: // Fall through. case Range::AngleBracketInner: // Fall through. case Range::AngleBracketAround: // Fall through. case Range::BraceInner: // Fall through. case Range::BraceAround: // Fall through. mayCrossBlock = true; V_FALLTHROUGH; case Range::WordAround: // Fall through. case Range::WordInner: // Fall through. case Range::WORDAround: // Fall through. case Range::WORDInner: // Fall through. case Range::QuoteInner: // Fall through. case Range::QuoteAround: // Fall through. case Range::DoubleQuoteInner: // Fall through. case Range::DoubleQuoteAround: // Fall through. case Range::BackQuoteInner: // Fall through. case Range::BackQuoteAround: { if (cursor.hasSelection()) { bool multipleBlocks = false; int nrBlock = VEditUtils::selectedBlockCount(cursor); if (mayCrossBlock && nrBlock > 1) { multipleBlocks = true; } // No need to add new line even crossing multiple blocks. copySelectedText(cursor, false); if (multipleBlocks) { message(tr("%1 %2 yanked").arg(nrBlock).arg(nrBlock > 1 ? tr("lines") : tr("line"))); } } break; } default: return; } } if (cursor.position() != oriPos) { cursor.setPosition(oriPos); changed = true; } cursor.endEditBlock(); goto exit; } V_ASSERT(to.isMovement()); // Filter out not supported movement for Copy action. switch (to.m_movement) { case Movement::PageUp: case Movement::PageDown: case Movement::HalfPageUp: case Movement::HalfPageDown: return; default: break; } cursor.beginEditBlock(); changed = processMovement(cursor, moveMode, to, repeat); if (repeat == -1) { repeat = 1; } if (changed) { bool addNewLine = false; switch (to.m_movement) { case Movement::Up: { expandSelectionToWholeLines(cursor); addNewLine = true; qDebug() << "copy up" << repeat << "lines"; break; } case Movement::Down: { expandSelectionToWholeLines(cursor); addNewLine = true; qDebug() << "copy down" << repeat << "lines"; break; } case Movement::EndOfLine: { // End of line (block). // Do not need to add new line even if repeat > 1. qDebug() << "copy till end of" << repeat << "line"; break; } case Movement::LineJump: { expandSelectionToWholeLines(cursor); addNewLine = true; qDebug() << "copy till line" << repeat; break; } case Movement::StartOfDocument: { expandSelectionToWholeLines(cursor); addNewLine = true; qDebug() << "copy till start of document"; break; } case Movement::EndOfDocument: { expandSelectionToWholeLines(cursor); addNewLine = true; qDebug() << "copy till end of document"; break; } default: break; } if (addNewLine) { int nrBlock = VEditUtils::selectedBlockCount(cursor); message(tr("%1 %2 yanked").arg(nrBlock).arg(nrBlock > 1 ? tr("lines") : tr("line"))); } copySelectedText(cursor, addNewLine); if (cursor.position() != oriPos) { cursor.setPosition(oriPos); } } cursor.endEditBlock(); exit: if (changed) { m_editor->setTextCursorW(cursor); } } void VVim::processPasteAction(QList &p_tokens, bool p_pasteBefore) { int repeat = 1; if (!p_tokens.isEmpty()) { Token to = p_tokens.takeFirst(); if (!p_tokens.isEmpty() || !to.isRepeat()) { p_tokens.clear(); return; } repeat = to.m_repeat; } Register ® = getRegister(m_regName); QString value = reg.read(); bool isBlock = reg.isBlock(); if (value.isEmpty()) { if (checkMode(VimMode::Visual) || checkMode(VimMode::VisualLine)) { setMode(VimMode::Normal); } return; } if (!(checkMode(VimMode::Normal) || checkMode(VimMode::Visual) || checkMode(VimMode::VisualLine))) { return; } QString text; text.reserve(repeat * value.size() + 1); for (int i = 0; i < repeat; ++i) { text.append(value); } bool changed = false; int nrBlock = 0; int restorePos = -1; QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); // Different from Vim: // In visual mode, by default vim select the current char, so paste operation will replace // current char, but here it is strange to follow this specification. bool isVisualLine = checkMode(VimMode::VisualLine); if (!checkMode(VimMode::Normal)) { // Visual or VisualLine mode. if (cursor.hasSelection()) { int pos = cursor.selectionStart(); deleteSelectedText(cursor, isVisualLine); if (isVisualLine) { // Insert a new block for insertion. insertChangeBlockAfterDeletion(cursor, pos); restorePos = cursor.position(); if (isBlock) { nrBlock = text.count('\n'); // insertChangeBlockAfterDeletion() already insert a new line, so eliminate one here. text = text.left(text.size() - 1); } } else if (isBlock) { // Insert new block right at current cursor. nrBlock = text.count('\n'); cursor.insertBlock(); restorePos = cursor.position(); } else if (text.count('\n') > 0) { restorePos = cursor.position(); } changed = true; } } else { // Normal mode. if (isBlock) { if (p_pasteBefore) { cursor.movePosition(QTextCursor::StartOfBlock); cursor.insertBlock(); cursor.movePosition(QTextCursor::PreviousBlock); } else { cursor.movePosition(QTextCursor::EndOfBlock); cursor.insertBlock(); } restorePos = cursor.position(); nrBlock = text.count('\n'); // inserBlock() already insert a new line, so eliminate one here. text = text.left(text.size() - 1); } else { // Not a block. if (!p_pasteBefore && !cursor.atBlockEnd()) { // Insert behind current cursor. cursor.movePosition(QTextCursor::Right); } if (text.count('\n') > 0) { restorePos = cursor.position(); } } changed = true; } if (changed) { cursor.insertText(text); if (restorePos == -1) { // Move cursor one character left. cursor.movePosition(QTextCursor::Left); } else { // Move cursor at the right position. cursor.setPosition(restorePos); } if (nrBlock > 0) { message(tr("%1 more %2").arg(nrBlock).arg(nrBlock > 1 ? tr("lines") : tr("line"))); } } cursor.endEditBlock(); if (!checkMode(VimMode::Normal)) { setMode(VimMode::Normal); } if (changed) { m_editor->setTextCursorW(cursor); } qDebug() << "text pasted" << text; } void VVim::processChangeAction(QList &p_tokens) { Token to = p_tokens.takeFirst(); int repeat = -1; if (to.isRepeat()) { repeat = to.m_repeat; to = p_tokens.takeFirst(); } if ((!to.isMovement() && !to.isRange()) || !p_tokens.isEmpty()) { p_tokens.clear(); return; } QTextCursor cursor = m_editor->textCursorW(); QTextDocument *doc = m_editor->documentW(); bool hasMoved = false; QTextCursor::MoveMode moveMode = QTextCursor::KeepAnchor; if (to.isRange()) { cursor.beginEditBlock(); hasMoved = selectRange(cursor, doc, to.m_range, repeat); if (hasMoved) { int pos = cursor.selectionStart(); switch (to.m_range) { case Range::Line: { // cc, change current line. if (cursor.hasSelection()) { deleteSelectedText(cursor, true); insertChangeBlockAfterDeletion(cursor, pos); } else { saveToRegister("\n"); } break; } case Range::ParenthesisInner: // Fall through. case Range::ParenthesisAround: // Fall through. case Range::BracketInner: // Fall through. case Range::BracketAround: // Fall through. case Range::AngleBracketInner: // Fall through. case Range::AngleBracketAround: // Fall through. case Range::BraceInner: // Fall through. case Range::BraceAround: // Fall through. case Range::WordAround: // Fall through. case Range::WordInner: // Fall through. case Range::WORDAround: // Fall through. case Range::WORDInner: // Fall through. case Range::QuoteInner: // Fall through. case Range::QuoteAround: // Fall through. case Range::DoubleQuoteInner: // Fall through. case Range::DoubleQuoteAround: // Fall through. case Range::BackQuoteInner: // Fall through. case Range::BackQuoteAround: { if (cursor.hasSelection()) { deleteSelectedText(cursor, false); } break; } default: return; } } cursor.endEditBlock(); goto exit; } V_ASSERT(to.isMovement()); // Filter out not supported movement for Change action. switch (to.m_movement) { case Movement::PageUp: case Movement::PageDown: case Movement::HalfPageUp: case Movement::HalfPageDown: return; default: break; } cursor.beginEditBlock(); if (checkMode(VimMode::VisualLine) || checkMode(VimMode::Visual)) { // Visual mode, omitting repeat and movement. // Different from Vim: // If there is no selection in Visual mode, we do nothing. bool visualLine = checkMode(VimMode::VisualLine); if (cursor.hasSelection()) { hasMoved = true; int pos = cursor.selectionStart(); deleteSelectedText(cursor, visualLine); if (visualLine) { insertChangeBlockAfterDeletion(cursor, pos); } } else if (visualLine) { hasMoved = true; saveToRegister("\n"); } cursor.endEditBlock(); goto exit; } hasMoved = processMovement(cursor, moveMode, to, repeat); if (repeat == -1) { repeat = 1; } if (hasMoved) { bool clearEmptyBlock = false; switch (to.m_movement) { case Movement::Left: { qDebug() << "change backward" << repeat << "chars"; break; } case Movement::Right: { qDebug() << "change forward" << repeat << "chars"; break; } case Movement::Up: { expandSelectionToWholeLines(cursor); clearEmptyBlock = true; qDebug() << "change up" << repeat << "lines"; break; } case Movement::Down: { expandSelectionToWholeLines(cursor); clearEmptyBlock = true; qDebug() << "change down" << repeat << "lines"; break; } case Movement::VisualUp: { qDebug() << "change visual up" << repeat << "lines"; break; } case Movement::VisualDown: { qDebug() << "change visual down" << repeat << "lines"; break; } case Movement::StartOfLine: { qDebug() << "change till start of line"; break; } case Movement::EndOfLine: { // End of line (block). if (repeat > 1) { clearEmptyBlock = true; } qDebug() << "change till end of" << repeat << "line"; break; } case Movement::FirstCharacter: { qDebug() << "change till first non-space character"; break; } case Movement::LineJump: { expandSelectionToWholeLines(cursor); clearEmptyBlock = true; qDebug() << "change till line" << repeat; break; } case Movement::StartOfDocument: { expandSelectionToWholeLines(cursor); clearEmptyBlock = true; qDebug() << "change till start of document"; break; } case Movement::EndOfDocument: { expandSelectionToWholeLines(cursor); clearEmptyBlock = true; qDebug() << "change till end of document"; break; } case Movement::WordForward: { qDebug() << "change" << repeat << "words forward"; break; } case Movement::WORDForward: { qDebug() << "change" << repeat << "WORDs forward"; break; } case Movement::ForwardEndOfWord: { qDebug() << "change" << repeat << "end of words forward"; break; } case Movement::ForwardEndOfWORD: { qDebug() << "change" << repeat << "end of WORDs forward"; break; } case Movement::WordBackward: { qDebug() << "change" << repeat << "words backward"; break; } case Movement::WORDBackward: { qDebug() << "change" << repeat << "WORDs backward"; break; } case Movement::BackwardEndOfWord: { qDebug() << "change" << repeat << "end of words backward"; break; } case Movement::BackwardEndOfWORD: { qDebug() << "change" << repeat << "end of WORDs backward"; break; } default: break; } if (cursor.hasSelection()) { int pos = cursor.selectionStart(); bool allDeleted = false; if (pos == 0) { QTextBlock block = m_editor->documentW()->lastBlock(); if (block.position() + block.length() - 1 == cursor.selectionEnd()) { allDeleted = true; } } deleteSelectedText(cursor, clearEmptyBlock); if (clearEmptyBlock && !allDeleted) { insertChangeBlockAfterDeletion(cursor, pos); } } } cursor.endEditBlock(); exit: if (hasMoved) { m_editor->setTextCursorW(cursor); } setMode(VimMode::Insert); } void VVim::processIndentAction(QList &p_tokens, IndentType p_type) { Token to = p_tokens.takeFirst(); int repeat = -1; if (to.isRepeat()) { repeat = to.m_repeat; to = p_tokens.takeFirst(); } if ((!to.isMovement() && !to.isRange()) || !p_tokens.isEmpty()) { p_tokens.clear(); return; } QTextCursor cursor = m_editor->textCursorW(); QString op; switch (p_type) { case IndentType::Indent: op = ">"; break; case IndentType::UnIndent: op = "<"; break; case IndentType::AutoIndent: op = "="; break; default: Q_ASSERT(false); } if (to.isRange()) { bool changed = selectRange(cursor, m_editor->documentW(), to.m_range, repeat); if (changed) { switch (to.m_range) { case Range::Line: { // >>/<<, indent/unindent current line. if (repeat == -1) { repeat = 1; } if (p_type == IndentType::AutoIndent) { VEditUtils::indentSelectedBlocksAsBlock(cursor, false); } else { VEditUtils::indentSelectedBlocks(cursor, m_editConfig->m_tabSpaces, p_type == IndentType::Indent); } message(tr("%1 %2 %3ed 1 time").arg(repeat) .arg(repeat > 1 ? tr("lines") : tr("line")) .arg(op)); break; } case Range::ParenthesisInner: // Fall through. case Range::ParenthesisAround: // Fall through. case Range::BracketInner: // Fall through. case Range::BracketAround: // Fall through. case Range::AngleBracketInner: // Fall through. case Range::AngleBracketAround: // Fall through. case Range::BraceInner: // Fall through. case Range::BraceAround: // Fall through. case Range::WordAround: // Fall through. case Range::WordInner: // Fall through. case Range::WORDAround: // Fall through. case Range::WORDInner: // Fall through. case Range::QuoteInner: // Fall through. case Range::QuoteAround: // Fall through. case Range::DoubleQuoteInner: // Fall through. case Range::DoubleQuoteAround: // Fall through. case Range::BackQuoteInner: // Fall through. case Range::BackQuoteAround: { int nrBlock = VEditUtils::selectedBlockCount(cursor); if (p_type == IndentType::AutoIndent) { VEditUtils::indentSelectedBlocksAsBlock(cursor, false); } else { VEditUtils::indentSelectedBlocks(cursor, m_editConfig->m_tabSpaces, p_type == IndentType::Indent); } message(tr("%1 %2 %3ed 1 time").arg(nrBlock) .arg(nrBlock > 1 ? tr("lines") : tr("line")) .arg(op)); break; } default: return; } } return; } V_ASSERT(to.isMovement()); // Filter out not supported movement for Indent/UnIndent action. switch (to.m_movement) { case Movement::PageUp: case Movement::PageDown: case Movement::HalfPageUp: case Movement::HalfPageDown: return; default: break; } if (checkMode(VimMode::VisualLine) || checkMode(VimMode::Visual)) { // Visual mode, omitting repeat and movement. // Different from Vim: // Do not exit Visual mode after indentation/unindentation. if (p_type == IndentType::AutoIndent) { VEditUtils::indentSelectedBlocksAsBlock(cursor, false); } else { VEditUtils::indentSelectedBlocks(cursor, m_editConfig->m_tabSpaces, p_type == IndentType::Indent); } return; } processMovement(cursor, QTextCursor::KeepAnchor, to, repeat); int nrBlock = VEditUtils::selectedBlockCount(cursor); if (p_type == IndentType::AutoIndent) { VEditUtils::indentSelectedBlocksAsBlock(cursor, false); } else { VEditUtils::indentSelectedBlocks(cursor, m_editConfig->m_tabSpaces, p_type == IndentType::Indent); } message(tr("%1 %2 %3ed 1 time").arg(nrBlock) .arg(nrBlock > 1 ? tr("lines") : tr("line")) .arg(op)); } void VVim::processToLowerAction(QList &p_tokens, bool p_toLower) { Token to = p_tokens.takeFirst(); int repeat = -1; if (to.isRepeat()) { repeat = to.m_repeat; to = p_tokens.takeFirst(); } if ((!to.isMovement() && !to.isRange()) || !p_tokens.isEmpty()) { p_tokens.clear(); return; } QTextCursor cursor = m_editor->textCursorW(); QTextDocument *doc = m_editor->documentW(); bool changed = false; QTextCursor::MoveMode moveMode = QTextCursor::KeepAnchor; int oriPos = cursor.position(); if (to.isRange()) { cursor.beginEditBlock(); changed = selectRange(cursor, doc, to.m_range, repeat); if (changed) { oriPos = cursor.selectionStart(); int nrBlock = VEditUtils::selectedBlockCount(cursor); message(tr("%1 %2 changed").arg(nrBlock) .arg(nrBlock > 1 ? tr("lines") : tr("line"))); convertCaseOfSelectedText(cursor, p_toLower); cursor.setPosition(oriPos); } cursor.endEditBlock(); goto exit; } V_ASSERT(to.isMovement()); // Filter out not supported movement for ToLower/ToUpper action. switch (to.m_movement) { case Movement::PageUp: case Movement::PageDown: case Movement::HalfPageUp: case Movement::HalfPageDown: return; default: break; } cursor.beginEditBlock(); changed = processMovement(cursor, moveMode, to, repeat); if (repeat == -1) { repeat = 1; } if (changed) { oriPos = cursor.selectionStart(); switch (to.m_movement) { case Movement::Up: { expandSelectionToWholeLines(cursor); break; } case Movement::Down: { expandSelectionToWholeLines(cursor); break; } case Movement::LineJump: { expandSelectionToWholeLines(cursor); break; } case Movement::StartOfDocument: { expandSelectionToWholeLines(cursor); break; } case Movement::EndOfDocument: { expandSelectionToWholeLines(cursor); break; } default: break; } int nrBlock = VEditUtils::selectedBlockCount(cursor); message(tr("%1 %2 changed").arg(nrBlock).arg(nrBlock > 1 ? tr("lines") : tr("line"))); convertCaseOfSelectedText(cursor, p_toLower); cursor.setPosition(oriPos); } cursor.endEditBlock(); exit: if (changed) { m_editor->setTextCursorW(cursor); } } void VVim::processUndoAction(QList &p_tokens) { int repeat = 1; if (!p_tokens.isEmpty()) { Token to = p_tokens.takeFirst(); if (!p_tokens.isEmpty() || !to.isRepeat()) { p_tokens.clear(); return; } repeat = to.m_repeat; } QTextDocument *doc = m_editor->documentW(); int i = 0; for (i = 0; i < repeat && doc->isUndoAvailable(); ++i) { m_editor->undoW(); } message(tr("Undo %1 %2").arg(i).arg(i > 1 ? tr("changes") : tr("change"))); } void VVim::processRedoAction(QList &p_tokens) { int repeat = 1; if (!p_tokens.isEmpty()) { Token to = p_tokens.takeFirst(); if (!p_tokens.isEmpty() || !to.isRepeat()) { p_tokens.clear(); return; } repeat = to.m_repeat; } QTextDocument *doc = m_editor->documentW(); int i = 0; for (i = 0; i < repeat && doc->isRedoAvailable(); ++i) { m_editor->redoW(); } message(tr("Redo %1 %2").arg(i).arg(i > 1 ? tr("changes") : tr("change"))); } void VVim::processRedrawLineAction(QList &p_tokens, int p_dest) { QTextCursor cursor = m_editor->textCursorW(); int repeat = cursor.block().blockNumber(); if (!p_tokens.isEmpty()) { Token to = p_tokens.takeFirst(); if (!p_tokens.isEmpty() || !to.isRepeat()) { p_tokens.clear(); return; } repeat = to.m_repeat - 1; } m_editor->scrollBlockInPage(repeat, p_dest); } void VVim::processJumpLocationAction(QList &p_tokens, bool p_next) { int repeat = 1; if (!p_tokens.isEmpty()) { Token to = p_tokens.takeFirst(); if (!p_tokens.isEmpty() || !to.isRepeat()) { p_tokens.clear(); return; } repeat = to.m_repeat; } QTextCursor cursor = m_editor->textCursorW(); Location loc; if (p_next) { while (m_locations.hasNext() && repeat > 0) { --repeat; loc = m_locations.nextLocation(); } } else { while (m_locations.hasPrevious() && repeat > 0) { --repeat; loc = m_locations.previousLocation(cursor); } } if (loc.isValid()) { QTextDocument *doc = m_editor->documentW(); if (loc.m_blockNumber >= doc->blockCount()) { message(tr("Mark has invalid line number")); return; } QTextBlock block = doc->findBlockByNumber(loc.m_blockNumber); int pib = loc.m_positionInBlock; if (pib >= block.length()) { pib = block.length() - 1; } if (!m_editor->isBlockVisible(block)) { // Scroll the block to the center of screen. m_editor->scrollBlockInPage(block.blockNumber(), 1); } cursor.setPosition(block.position() + pib); m_editor->setTextCursorW(cursor); } } 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->textCursorW(); 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->setTextCursorW(cursor); setMode(VimMode::Normal); } } void VVim::processReverseCaseAction(QList &p_tokens) { int repeat = 1; if (!p_tokens.isEmpty()) { Token to = p_tokens.takeFirst(); Q_ASSERT(to.isRepeat() && p_tokens.isEmpty()); repeat = to.m_repeat; } if (!(checkMode(VimMode::Normal) || checkMode(VimMode::Visual) || checkMode(VimMode::VisualLine))) { return; } // Reverse the next repeat characters' case until the end of line. // If repeat is greater than the number of left characters in current line, // just change the actual number of left characters. // In visual mode, repeat is ignored and reverse the selected text. QTextCursor cursor = m_editor->textCursorW(); 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; cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, repeat > nrChar ? nrChar : repeat); } bool changed = reverseSelectedTextCase(cursor); cursor.endEditBlock(); if (changed) { m_editor->setTextCursorW(cursor); setMode(VimMode::Normal); } } void VVim::processJoinAction(QList &p_tokens, bool p_modifySpaces) { int repeat = 2; if (!p_tokens.isEmpty()) { Token to = p_tokens.takeFirst(); Q_ASSERT(to.isRepeat() && p_tokens.isEmpty()); repeat = qMax(to.m_repeat, repeat); } if (!(checkMode(VimMode::Normal) || checkMode(VimMode::Visual) || checkMode(VimMode::VisualLine))) { return; } // Join repeat lines, with the minimum of two lines. Do nothing when on the // last line. // If @p_modifySpaces is true, remove the indent and insert up to two spaces. // If repeat is too big, it is reduced to the number of lines available. // In visual mode, repeat is ignored and join the highlighted lines. int firstBlock = -1; int blockCount = repeat; QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); if (checkMode(VimMode::Normal)) { firstBlock = cursor.block().blockNumber(); } else { QTextDocument *doc = m_editor->documentW(); firstBlock = doc->findBlock(cursor.selectionStart()).blockNumber(); int lastBlock = doc->findBlock(cursor.selectionEnd()).blockNumber(); blockCount = lastBlock - firstBlock + 1; } bool changed = joinLines(cursor, firstBlock, blockCount, p_modifySpaces); cursor.endEditBlock(); if (changed) { m_editor->setTextCursorW(cursor); setMode(VimMode::Normal); } } bool VVim::clearSelection() { QTextCursor cursor = m_editor->textCursorW(); if (cursor.hasSelection()) { cursor.clearSelection(); m_editor->setTextCursorW(cursor); return true; } return false; } int VVim::blockCountOfPageStep() const { int lineCount = m_editor->documentW()->blockCount(); QScrollBar *bar = m_editor->verticalScrollBarW(); int steps = (bar->maximum() - bar->minimum() + bar->pageStep()); int pageLineCount = lineCount * (bar->pageStep() * 1.0 / steps); return pageLineCount; } void VVim::maintainSelectionInVisualMode(QTextCursor *p_cursor) { // We need to always select the character on current position. QTextCursor *cursor = p_cursor; QTextCursor tmpCursor = m_editor->textCursorW(); if (!cursor) { cursor = &tmpCursor; } bool hasChanged = false; int pos = cursor->position(); int anchor = cursor->anchor(); if (pos > anchor) { Q_ASSERT(pos > m_positionBeforeVisualMode); if (anchor > m_positionBeforeVisualMode) { // Re-select. cursor->setPosition(m_positionBeforeVisualMode); cursor->setPosition(pos, QTextCursor::KeepAnchor); hasChanged = true; } setCursorBlockMode(m_editor, CursorBlock::LeftSide); } else if (pos == anchor) { Q_ASSERT(anchor >= m_positionBeforeVisualMode); // Re-select. if (anchor == m_positionBeforeVisualMode) { cursor->setPosition(m_positionBeforeVisualMode + 1); cursor->setPosition(pos, QTextCursor::KeepAnchor); hasChanged = true; setCursorBlockMode(m_editor, CursorBlock::RightSide); } else { cursor->setPosition(m_positionBeforeVisualMode); cursor->setPosition(pos, QTextCursor::KeepAnchor); hasChanged = true; setCursorBlockMode(m_editor, CursorBlock::LeftSide); } } else { // Re-select. if (anchor <= m_positionBeforeVisualMode) { cursor->setPosition(m_positionBeforeVisualMode + 1); cursor->setPosition(pos, QTextCursor::KeepAnchor); hasChanged = true; } setCursorBlockMode(m_editor, CursorBlock::RightSide); } if (hasChanged && !p_cursor) { m_editor->setTextCursorW(*cursor); } } void VVim::expandSelectionToWholeLines(QTextCursor &p_cursor) { QTextDocument *doc = m_editor->documentW(); int curPos = p_cursor.position(); int anchorPos = p_cursor.anchor(); QTextBlock curBlock = doc->findBlock(curPos); QTextBlock anchorBlock = doc->findBlock(anchorPos); if (curPos >= anchorPos) { p_cursor.setPosition(anchorBlock.position(), QTextCursor::MoveAnchor); p_cursor.setPosition(curBlock.position() + curBlock.length() - 1, QTextCursor::KeepAnchor); } else { p_cursor.setPosition(anchorBlock.position() + anchorBlock.length() - 1, QTextCursor::MoveAnchor); p_cursor.setPosition(curBlock.position(), QTextCursor::KeepAnchor); } } void VVim::initRegisters() { if (!s_registers.isEmpty()) { return; } for (char ch = 'a'; ch <= 'z'; ++ch) { s_registers[QChar(ch)] = Register(QChar(ch)); } s_registers[c_unnamedRegister] = Register(c_unnamedRegister); s_registers[c_blackHoleRegister] = Register(c_blackHoleRegister); s_registers[c_selectionRegister] = Register(c_selectionRegister); } bool VVim::expectingRegisterName() const { return m_keys.size() == 1 && 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)); } bool VVim::expectingReplaceCharacter() const { return m_keys.size() == 1 && m_keys.first() == Key(Qt::Key_R, Qt::NoModifier); } bool VVim::expectingLeaderSequence() const { if (m_replayLeaderSequence || m_keys.isEmpty()) { return false; } return m_keys.first() == Key(Qt::Key_Backslash); } bool VVim::expectingMarkName() const { return checkPendingKey(Key(Qt::Key_M)) && m_tokens.isEmpty(); } bool VVim::expectingMarkTarget() const { return checkPendingKey(Key(Qt::Key_Apostrophe)) || checkPendingKey(Key(Qt::Key_QuoteLeft)); } QChar VVim::keyToRegisterName(const Key &p_key) const { if (p_key.isAlphabet()) { return p_key.toAlphabet().toLower(); } switch (p_key.m_key) { case Qt::Key_QuoteDbl: if (p_key.m_modifiers == Qt::ShiftModifier) { return c_unnamedRegister; } break; case Qt::Key_Plus: if (p_key.m_modifiers == Qt::ShiftModifier) { return c_selectionRegister; } break; case Qt::Key_Underscore: if (p_key.m_modifiers == Qt::ShiftModifier) { return c_blackHoleRegister; } break; default: break; } return QChar(); } bool VVim::hasActionToken() const { // There will be only one action token and it is placed at the front. bool has = false; if (m_tokens.isEmpty()) { return false; } if (m_tokens.at(0).isAction()) { has = true; } for (int i = 1; i < m_tokens.size(); ++i) { V_ASSERT(!m_tokens.at(i).isAction()); } return has; } bool VVim::hasRepeatToken() const { // There will be only one repeat token. bool has = false; if (m_tokens.isEmpty()) { return false; } for (int i = 0; i < m_tokens.size(); ++i) { if (m_tokens.at(i).isRepeat()) { V_ASSERT(!has); has = true; } } return has; } bool VVim::hasActionTokenValidForTextObject() const { if (hasActionToken()) { Action act = m_tokens.first().m_action; if (act == Action::Delete || act == Action::Copy || act == Action::Change || act == Action::ToLower || act == Action::ToUpper || act == Action::Indent || act == Action::UnIndent) { return true; } } return false; } bool VVim::checkActionToken(Action p_action) const { if (hasActionToken()) { return m_tokens.first().m_action == p_action; } return false; } bool VVim::checkPendingKey(const Key &p_key) const { return (m_keys.size() == 1 && m_keys.first() == p_key); } void VVim::tryAddMoveAction() { if (!hasActionToken()) { addActionToken(Action::Move); } } void VVim::addActionToken(Action p_action) { V_ASSERT(!hasActionToken()); m_tokens.prepend(Token(p_action)); } const VVim::Token *VVim::getActionToken() const { V_ASSERT(hasActionToken()); return &m_tokens.first(); } VVim::Token *VVim::getRepeatToken() { V_ASSERT(hasRepeatToken()); for (auto & token : m_tokens) { if (token.isRepeat()) { return &token; } } return NULL; } void VVim::addRangeToken(Range p_range) { m_tokens.append(Token(p_range)); } 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::addKeyToken(Key p_key) { m_tokens.append(Token(p_key)); } void VVim::deleteSelectedText(QTextCursor &p_cursor, bool p_clearEmptyBlock) { if (p_cursor.hasSelection()) { QString deletedText = VEditUtils::selectedText(p_cursor); p_cursor.removeSelectedText(); if (p_clearEmptyBlock && p_cursor.block().length() == 1) { deletedText += "\n"; VEditUtils::removeBlock(p_cursor); } saveToRegister(deletedText); } } void VVim::copySelectedText(bool p_addNewLine) { QTextCursor cursor = m_editor->textCursorW(); if (cursor.hasSelection()) { cursor.beginEditBlock(); copySelectedText(cursor, p_addNewLine); cursor.endEditBlock(); m_editor->setTextCursorW(cursor); } } void VVim::copySelectedText(QTextCursor &p_cursor, bool p_addNewLine) { if (p_cursor.hasSelection()) { QString text = VEditUtils::selectedText(p_cursor); p_cursor.clearSelection(); if (p_addNewLine) { text += "\n"; } saveToRegister(text); } } void VVim::convertCaseOfSelectedText(QTextCursor &p_cursor, bool p_toLower) { if (p_cursor.hasSelection()) { QTextDocument *doc = p_cursor.document(); int start = p_cursor.selectionStart(); int end = p_cursor.selectionEnd(); p_cursor.clearSelection(); p_cursor.setPosition(start); int pos = p_cursor.position(); while (pos < end) { QChar ch = doc->characterAt(pos); bool modified = false; if (p_toLower) { if (ch.isUpper()) { ch = ch.toLower(); modified = true; } } else if (ch.isLower()) { ch = ch.toUpper(); modified = true; } if (modified) { p_cursor.deleteChar(); p_cursor.insertText(ch); } else { p_cursor.movePosition(QTextCursor::NextCharacter); } pos = p_cursor.position(); } } } void VVim::saveToRegister(const QString &p_text) { QString text(p_text); VEditUtils::removeObjectReplacementCharacter(text); qDebug() << QString("save text(%1) to register(%2)").arg(text).arg(m_regName); Register ® = getRegister(m_regName); reg.update(text); if (!reg.isBlackHoleRegister() && !reg.isUnnamedRegister()) { // Save it to unnamed register. setRegister(c_unnamedRegister, reg.m_value); } } void VVim::Register::update(const QString &p_value) { QChar newLine('\n'); bool newIsBlock = false; if (p_value.endsWith(newLine)) { newIsBlock = true; } bool oriIsBlock = isBlock(); if (isNamedRegister() && m_append) { // Append @p_value to m_value. if (newIsBlock) { if (oriIsBlock) { m_value += p_value; } else { m_value.append(newLine); m_value += p_value; } } else if (oriIsBlock) { m_value += p_value; m_value.append(newLine); } else { m_value += p_value; } } else { // Set m_value to @p_value. m_value = p_value; } if (isSelectionRegister()) { // Change system clipboard. QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(m_value); } } const QString &VVim::Register::read() { if (isSelectionRegister()) { // Update from system clipboard. QClipboard *clipboard = QApplication::clipboard(); const QMimeData *mimeData = clipboard->mimeData(); if (mimeData->hasText()) { m_value = mimeData->text(); } else { m_value.clear(); } } 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); } void VVim::message(const QString &p_msg) { if (!p_msg.isEmpty()) { qDebug() << "vim msg:" << p_msg; emit vimMessage(p_msg); } } const QMap &VVim::getRegisters() const { return s_registers; } const VVim::Marks &VVim::getMarks() const { return m_marks; } QChar VVim::getCurrentRegisterName() const { return m_regName; } QString VVim::getPendingKeys() const { QString str; for (auto const & key : m_pendingKeys) { str.append(keyToString(key.m_key, key.m_modifiers)); } return str; } void VVim::setCurrentRegisterName(QChar p_reg) { m_regName = p_reg; } bool VVim::checkMode(VimMode p_mode) { return m_mode == p_mode; } bool VVim::processCommandLine(VVim::CommandLineType p_type, const QString &p_cmd) { setMode(VimMode::Normal); 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; } default: break; } 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); } } void VVim::processCommandLineCancelled() { m_searchHistory.resetIndex(); m_editor->clearIncrementalSearchedWordHighlight(); } VVim::SearchItem VVim::fetchSearchItem(VVim::CommandLineType p_type, const QString &p_cmd) { Q_ASSERT(p_type == CommandLineType::SearchForward || p_type == CommandLineType::SearchBackward); SearchItem item; item.m_rawStr = p_cmd; item.m_text = p_cmd; item.m_forward = p_type == CommandLineType::SearchForward; if (p_cmd.indexOf("\\C") > -1) { item.m_options |= FindOption::CaseSensitive; item.m_text.remove("\\C"); } item.m_options |= FindOption::RegularExpression; return item; } bool VVim::executeCommand(const QString &p_cmd) { bool validCommand = true; QString msg; 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->object()->saveNote(); msg = tr("Note has been saved"); } else if (p_cmd == "q") { // :q, quit edit mode. emit m_editor->object()->discardAndRead(); msg = tr("Quit"); } else if (p_cmd == "x") { // :x, save if there is any change and quit edit mode. emit m_editor->object()->saveAndRead(); msg = tr("Quit with note having been saved"); } else { validCommand = false; } } 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->object()->saveAndRead(); msg = tr("Quit with note having been saved"); } else if (p_cmd == "q!") { // :q!, discard change and quit edit mode. emit m_editor->object()->discardAndRead(); msg = tr("Quit"); } else { validCommand = false; } } else if (p_cmd == "nohlsearch" || p_cmd == "noh") { // :nohlsearch, clear highlight search. clearSearchHighlight(); } else { validCommand = false; } 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. if (allDigits) { bool ok; int num = p_cmd.toInt(&ok, 10); if (num == 0) { num = 1; } if (ok && num > 0) { m_tokens.append(Token(num)); tryAddMoveAction(); addMovementToken(Movement::LineJump); processCommand(m_tokens); validCommand = true; } } } if (!validCommand) { message(tr("Not an editor command: %1").arg(p_cmd)); } else { message(msg); } return validCommand; } bool VVim::hasNonDigitPendingKeys(const QList &p_keys) { for (auto const &key : p_keys) { if (!key.isDigit()) { return true; } } return false; } bool VVim::hasNonDigitPendingKeys() { return hasNonDigitPendingKeys(m_keys); } bool VVim::processLeaderSequence(const Key &p_key) { // Different from Vim: // If it is not a valid sequence, we just do nothing here. V_ASSERT(checkPendingKey(Key(Qt::Key_Backslash))); bool validSequence = true; QList replaySeq; if (p_key == Key(Qt::Key_Y)) { // y, "+y replaySeq.append(Key(Qt::Key_QuoteDbl, Qt::ShiftModifier)); replaySeq.append(Key(Qt::Key_Plus, Qt::ShiftModifier)); replaySeq.append(Key(Qt::Key_Y)); } else if (p_key == Key(Qt::Key_D)) { // d, "+d replaySeq.append(Key(Qt::Key_QuoteDbl, Qt::ShiftModifier)); replaySeq.append(Key(Qt::Key_Plus, Qt::ShiftModifier)); replaySeq.append(Key(Qt::Key_D)); } else if (p_key == Key(Qt::Key_P)) { // p, "+p replaySeq.append(Key(Qt::Key_QuoteDbl, Qt::ShiftModifier)); replaySeq.append(Key(Qt::Key_Plus, Qt::ShiftModifier)); replaySeq.append(Key(Qt::Key_P)); } else if (p_key == Key(Qt::Key_P, Qt::ShiftModifier)) { // P, "+P 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)) { // , clear search highlight clearSearchHighlight(); } else if (p_key == Key(Qt::Key_W)) { // w, save note emit m_editor->object()->saveNote(); message(tr("Note has been saved")); } else { validSequence = false; } if (!replaySeq.isEmpty()) { // Replay the sequence. m_replayLeaderSequence = true; m_keys.clear(); for (int i = 0; i < 2; ++i) { m_pendingKeys.pop_back(); } for (auto const &key : replaySeq) { bool ret = handleKeyPressEvent(key.m_key, key.m_modifiers); if (!ret) { break; } } m_replayLeaderSequence = false; } else { resetState(); } return validSequence; } VVim::LocationStack::LocationStack(int p_maximum) : m_pointer(0), c_maximumLocations(p_maximum < 10 ? 10 : p_maximum) { } bool VVim::LocationStack::hasPrevious() const { return m_pointer > 0; } bool VVim::LocationStack::hasNext() const { return m_pointer < m_locations.size() - 1; } void VVim::LocationStack::addLocation(const QTextCursor &p_cursor) { int blockNumber = p_cursor.block().blockNumber(); int pib = p_cursor.positionInBlock(); // Remove older location with the same block number. for (auto it = m_locations.begin(); it != m_locations.end();) { if (it->m_blockNumber == blockNumber) { it = m_locations.erase(it); break; } else { ++it; } } if (m_locations.size() >= c_maximumLocations) { m_locations.removeFirst(); } m_locations.append(Location(blockNumber, pib)); m_pointer = m_locations.size(); qDebug() << QString("add location (%1,%2), pointer=%3") .arg(blockNumber).arg(pib).arg(m_pointer); } const VVim::Location &VVim::LocationStack::previousLocation(const QTextCursor &p_cursor) { V_ASSERT(hasPrevious()); if (m_pointer == m_locations.size()) { // Add current location to the stack. addLocation(p_cursor); m_pointer = m_locations.size() - 1; } // Jump to previous location. if (m_pointer > 0) { --m_pointer; } qDebug() << QString("previous location (%1,%2), pointer=%3, size=%4") .arg(m_locations.at(m_pointer).m_blockNumber) .arg(m_locations.at(m_pointer).m_positionInBlock) .arg(m_pointer).arg(m_locations.size()); return m_locations.at(m_pointer); } const VVim::Location &VVim::LocationStack::nextLocation() { V_ASSERT(hasNext()); ++m_pointer; qDebug() << QString("next location (%1,%2), pointer=%3, size=%4") .arg(m_locations.at(m_pointer).m_blockNumber) .arg(m_locations.at(m_pointer).m_positionInBlock) .arg(m_pointer).arg(m_locations.size()); return m_locations.at(m_pointer); } VVim::Marks::Marks() { for (char ch = 'a'; ch <= 'z'; ++ch) { m_marks[QChar(ch)] = Mark(); } } void VVim::Marks::setMark(QChar p_name, const QTextCursor &p_cursor) { auto it = m_marks.find(p_name); if (it == m_marks.end()) { return; } it->m_location.m_blockNumber = p_cursor.block().blockNumber(); it->m_location.m_positionInBlock = p_cursor.positionInBlock(); it->m_text = p_cursor.block().text().trimmed(); it->m_name = p_name; m_lastUsedMark = p_name; qDebug() << QString("set mark %1 to (%2,%3)") .arg(p_name) .arg(it->m_location.m_blockNumber) .arg(it->m_location.m_positionInBlock); } void VVim::Marks::clearMark(QChar p_name) { auto it = m_marks.find(p_name); if (it == m_marks.end()) { return; } it->m_location = Location(); it->m_text.clear(); if (m_lastUsedMark == p_name) { m_lastUsedMark = QChar(); } } VVim::Location VVim::Marks::getMarkLocation(QChar p_name) { auto it = m_marks.find(p_name); if (it == m_marks.end()) { return Location(); } if (it->m_location.isValid()) { m_lastUsedMark = p_name; } return it->m_location; } const QMap &VVim::Marks::getMarks() const { return m_marks; } QChar VVim::Marks::getLastUsedMark() const { return m_lastUsedMark; } void VVim::processTitleJump(const QList &p_tokens, bool p_forward, int p_relativeLevel) { int repeat = 1; if (p_tokens.size() == 1) { Token to = p_tokens.first(); if (to.isRepeat()) { repeat = to.m_repeat; } else { return; } } else if (!p_tokens.isEmpty()) { return; } QTextCursor cursor = m_editor->textCursorW(); if (m_editor->jumpTitle(p_forward, p_relativeLevel, repeat)) { // Record current location. 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(); } QString VVim::readRegister(int p_key, int p_modifiers) { Key keyInfo(p_key, p_modifiers); QChar reg = keyToRegisterName(keyInfo); if (!reg.isNull()) { return getRegister(reg).read(); } return ""; } void VVim::amendCursorPosition() { if (checkMode(VimMode::Normal)) { QTextCursor cursor = m_editor->textCursorW(); if (cursor.atBlockEnd() && !cursor.atBlockStart()) { // Normal mode and cursor at the end of a non-empty block. cursor.movePosition(QTextCursor::PreviousCharacter); m_editor->setTextCursorW(cursor); } } } void VVim::handleMousePressed(QMouseEvent *p_event) { Q_UNUSED(p_event); QTextCursor cursor = m_editor->textCursorW(); if ((checkMode(VimMode::Visual) || checkMode(VimMode::VisualLine)) && p_event->buttons() != Qt::RightButton) { setMode(VimMode::Normal); } else if (checkMode(VimMode::Normal)) { if (cursor.hasSelection()) { setMode(VimMode::Visual, false); maintainSelectionInVisualMode(); } } } void VVim::handleMouseMoved(QMouseEvent *p_event) { if (p_event->buttons() != Qt::LeftButton) { return; } QTextCursor cursor = m_editor->textCursorW(); if (cursor.hasSelection()) { if (checkMode(VimMode::Normal)) { int pos = cursor.position(); int anchor = cursor.anchor(); QTextBlock block = cursor.document()->findBlock(anchor); if (anchor > 0 && anchor == block.position() + block.length() - 1) { // Move anchor left. cursor.setPosition(anchor - 1); cursor.setPosition(pos, QTextCursor::KeepAnchor); m_editor->setTextCursorW(cursor); } setMode(VimMode::Visual, false); maintainSelectionInVisualMode(); } else if (checkMode(VimMode::Visual)) { // We need to assure we always select the character on m_positionBeforeVisualMode. maintainSelectionInVisualMode(); } } else if (checkMode(VimMode::Visual)) { // User move cursor in Visual mode. Now the cursor and anchor // are at the same position. maintainSelectionInVisualMode(); } } void VVim::handleMouseReleased(QMouseEvent *p_event) { if (checkMode(VimMode::Normal) && p_event->button() == Qt::LeftButton) { QTextCursor cursor = m_editor->textCursorW(); if (cursor.hasSelection()) { return; } amendCursorPosition(); } } void VVim::setCursorBlockMode(VEditor *p_cursor, CursorBlock p_mode) { p_cursor->setCursorBlockModeW(p_mode); } bool VVim::useLeftSideOfCursor(const QTextCursor &p_cursor) { if (!checkMode(VimMode::Visual)) { return false; } Q_ASSERT(m_positionBeforeVisualMode >= 0); return p_cursor.position() > m_positionBeforeVisualMode; } bool VVim::checkEnterNormalMode(int p_key, int p_modifiers) { if (p_key == Qt::Key_Escape) { return true; } if (!VUtils::isControlModifierForVim(p_modifiers)) { return false; } if (p_key == Qt::Key_BracketLeft) { return true; } if (p_key == Qt::Key_C && !g_config->getVimExemptionKeys().contains('c')) { return true; } return false; } void VVim::clearSelectionAndEnterNormalMode() { int position = -1; if (checkMode(VimMode::Visual)) { QTextCursor cursor = m_editor->textCursorW(); if (cursor.position() > cursor.anchor()) { position = cursor.position() - 1; } } bool ret = clearSelection(); if (!ret && checkMode(VimMode::Normal)) { emit m_editor->object()->requestCloseFindReplaceDialog(); } setMode(VimMode::Normal, true, position); }