#include "vvim.h" #include #include #include #include #include #include #include "vedit.h" #include "utils/veditutils.h" const QChar VVim::c_unnamedRegister = QChar('"'); const QChar VVim::c_blackHoleRegister = QChar('_'); const QChar VVim::c_selectionRegister = QChar('+'); VVim::VVim(VEdit *p_editor) : QObject(p_editor), m_editor(p_editor), m_editConfig(&p_editor->getConfig()), m_mode(VimMode::Normal), m_resetPositionInBlock(true), m_register(c_unnamedRegister) { connect(m_editor, &VEdit::copyAvailable, this, &VVim::selectionToVisualMode); } // 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); } } // Move @p_cursor to the first non-space character of current block. // Need to setTextCursor() after calling this. static void moveCursorFirstNonSpaceCharacter(QTextCursor &p_cursor, QTextCursor::MoveMode p_mode) { QTextBlock block = p_cursor.block(); QString text = block.text(); int idx = 0; for (; idx < text.size(); ++idx) { if (text[idx].isSpace()) { continue; } else { break; } } p_cursor.setPosition(block.position() + idx, p_mode); } // Find the start and end of the WORD @p_cursor locates in (within a single block). // @p_start and @p_end will be the global position of the start and end of the WORD. // @p_start will equals to @p_end if @p_cursor is a space. static void findCurrentWORD(const QTextCursor &p_cursor, int &p_start, int &p_end) { QTextBlock block = p_cursor.block(); QString text = block.text(); int pib = p_cursor.positionInBlock(); // Find the start. p_start = p_end = -1; for (int i = pib - 1; i >= 0; --i) { if (text[i].isSpace()) { ++i; p_start = i; break; } } if (p_start == -1) { p_start = 0; } // Find the end. for (int i = pib; i < text.size(); ++i) { if (text[i].isSpace()) { p_end = i; break; } } if (p_end == -1) { p_end = block.length() - 1; } p_start += block.position(); p_end += block.position(); } // Move @p_cursor to skip spaces if current cursor is placed at a space // (may move across blocks). It will stop by the empty block on the way. // Forward: wwwwsssss|wwww // Backward: wwww|ssssswwww static void moveCursorAcrossSpaces(QTextCursor &p_cursor, QTextCursor::MoveMode p_mode, bool p_forward) { while (true) { QTextBlock block = p_cursor.block(); QString text = block.text(); int pib = p_cursor.positionInBlock(); if (p_forward) { for (; pib < text.size(); ++pib) { if (!text[pib].isSpace()) { break; } } if (pib == text.size()) { // Move to next block. p_cursor.movePosition(QTextCursor::Down, p_mode, 1); if (block.blockNumber() == p_cursor.block().blockNumber()) { // Already at the last block. p_cursor.movePosition(QTextCursor::EndOfBlock, p_mode, 1); break; } else { p_cursor.movePosition(QTextCursor::StartOfBlock, p_mode, 1); if (p_cursor.block().length() <= 1) { break; } } } else { // Found non-space character. p_cursor.setPosition(block.position() + pib, p_mode); break; } } else { int idx = pib - 1; for (; idx >= 0; --idx) { if (!text[idx].isSpace()) { break; } } if (idx == -1) { // Move to previous block. p_cursor.movePosition(QTextCursor::Up, p_mode, 1); if (block.blockNumber() == p_cursor.block().blockNumber()) { // Already at the first block. p_cursor.movePosition(QTextCursor::StartOfBlock, p_mode, 1); break; } else { p_cursor.movePosition(QTextCursor::EndOfBlock, p_mode, 1); if (p_cursor.block().length() <= 1) { break; } } } else { // Found non-space character. p_cursor.setPosition(block.position() + idx + 1, p_mode); break; } } } } bool VVim::handleKeyPressEvent(QKeyEvent *p_event) { bool ret = false; int modifiers = p_event->modifiers(); int key = p_event->key(); bool resetPositionInBlock = true; Key keyInfo(key, modifiers); // Handle Insert mode key press. if (VimMode::Insert == m_mode) { if (key == Qt::Key_Escape || (key == Qt::Key_BracketLeft && modifiers == Qt::ControlModifier)) { // Clear selection and enter Normal mode. clearSelection(); setMode(VimMode::Normal); goto clear_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) { goto accept; } if (expectingRegisterName()) { // Expecting a register name. QChar reg = keyToRegisterName(keyInfo); if (!reg.isNull()) { resetState(); m_register = reg; m_registers[reg].m_append = (modifiers == Qt::ShiftModifier); qDebug() << "use register" << reg << m_registers[reg].m_append; goto accept; } goto clear_accept; } // We will add key to m_keys. If all m_keys can combined to a token, add // a new token to m_tokens, clear m_keys and try to process m_tokens. switch (key) { case Qt::Key_0: { if (modifiers == Qt::NoModifier) { if (!m_keys.isEmpty()) { // Repeat. V_ASSERT(m_keys.last().isDigit()); m_keys.append(keyInfo); resetPositionInBlock = false; goto accept; } else { // StartOfLine. tryAddMoveAction(); m_tokens.append(Token(Movement::StartOfLine)); processCommand(m_tokens); } } 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) { 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) { // 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(); m_tokens.append(Token(mm)); processCommand(m_tokens); resetPositionInBlock = false; } break; } case Qt::Key_I: { if (modifiers == Qt::NoModifier) { // Enter Insert mode. if (m_mode == VimMode::Normal) { setMode(VimMode::Insert); } } else if (modifiers == Qt::ShiftModifier) { QTextCursor cursor = m_editor->textCursor(); if (m_mode == VimMode::Normal) { // Insert at the first non-space character. 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->setTextCursor(cursor); setMode(VimMode::Insert); } break; } case Qt::Key_A: { if (modifiers == Qt::NoModifier) { // Enter Insert mode. // Move cursor back one character. if (m_mode == VimMode::Normal) { QTextCursor cursor = m_editor->textCursor(); V_ASSERT(!cursor.hasSelection()); if (!cursor.atBlockEnd()) { cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 1); m_editor->setTextCursor(cursor); } setMode(VimMode::Insert); } } else if (modifiers == Qt::ShiftModifier) { // Insert at the end of line. QTextCursor cursor = m_editor->textCursor(); if (m_mode == VimMode::Normal) { cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor, 1); m_editor->setTextCursor(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->setTextCursor(cursor); } } setMode(VimMode::Insert); } break; } case Qt::Key_O: { if (modifiers == Qt::NoModifier) { // Insert a new block under current block and enter insert mode. if (m_mode == VimMode::Normal) { QTextCursor cursor = m_editor->textCursor(); cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor, 1); cursor.insertBlock(); m_editor->setTextCursor(cursor); setMode(VimMode::Insert); } } else if (modifiers == Qt::ShiftModifier) { // Insert a new block above current block and enter insert mode. if (m_mode == VimMode::Normal) { QTextCursor cursor = m_editor->textCursor(); cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor, 1); cursor.insertBlock(); cursor.movePosition(QTextCursor::PreviousBlock, QTextCursor::MoveAnchor, 1); m_editor->setTextCursor(cursor); setMode(VimMode::Insert); } } break; } case Qt::Key_S: { if (modifiers == Qt::NoModifier) { // 1. If there is selection, delete the selected text. // 2. Otherwise, if cursor is not at the end of block, delete the // character after current cursor. // 3. Enter Insert mode. QTextCursor cursor = m_editor->textCursor(); if (cursor.hasSelection() || !cursor.atBlockEnd()) { cursor.deleteChar(); m_editor->setTextCursor(cursor); } setMode(VimMode::Insert); } 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 (modifiers == Qt::ControlModifier) { // Ctrl+B, page up, fall through. modifiers = Qt::NoModifier; } else if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) { tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty()) { // Not a valid sequence. break; } // b, go to the start of previous or current word. Movement mm = Movement::WordBackward; if (modifiers == Qt::ShiftModifier) { // B, go to the start of previous or current WORD. mm = Movement::WORDBackward; } tryAddMoveAction(); m_tokens.append(Token(mm)); processCommand(m_tokens); break; } else { break; } } 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: { if (modifiers == Qt::ControlModifier) { // Ctrl+U, HalfPageUp. tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty()) { // Not a valid sequence. break; } Movement mm = Movement::HalfPageUp; tryAddMoveAction(); m_tokens.append(Token(mm)); processCommand(m_tokens); resetPositionInBlock = false; } 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 (modifiers == Qt::ControlModifier) { // 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 (getActionToken()->m_action == Action::Delete) { addRangeToken(Range::Line); processCommand(m_tokens); break; } else { // An invalid sequence. break; } } else { // The first d, an Action. if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) { deleteSelectedText(m_mode == VimMode::VisualLine); setMode(VimMode::Normal); break; } addActionToken(Action::Delete); goto accept; } } else if (modifiers == Qt::ShiftModifier) { tryGetRepeatToken(m_keys, m_tokens); if (!hasActionToken() && m_mode == VimMode::Normal) { // D, same as d$. addActionToken(Action::Delete); addMovementToken(Movement::EndOfLine); processCommand(m_tokens); } break; } break; } // Should be kept together with Qt::Key_Escape. case Qt::Key_BracketLeft: { if (modifiers == Qt::ControlModifier) { // fallthrough. } else { break; } } case Qt::Key_Escape: { // Clear selection and enter normal mode. clearSelection(); setMode(VimMode::Normal); break; } case Qt::Key_V: { if (modifiers == Qt::NoModifier) { // Toggle Visual Mode. clearSelection(); VimMode mode = VimMode::Visual; if (m_mode == VimMode::Visual) { mode = VimMode::Normal; } setMode(mode); } 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->textCursor(); expandSelectionToWholeLines(cursor); m_editor->setTextCursor(cursor); } } 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) { tryGetRepeatToken(m_keys, m_tokens); if (!m_keys.isEmpty()) { // Not a valid sequence. break; } // w, go to the start of next word. Movement mm = Movement::WordForward; if (modifiers == Qt::ShiftModifier) { // W, go to the start of next WORD. mm = Movement::WORDForward; } tryAddMoveAction(); m_tokens.append(Token(mm)); processCommand(m_tokens); } break; } case Qt::Key_E: { // e, E, ge, gE. if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) { tryGetRepeatToken(m_keys, m_tokens); Movement mm = Movement::Invalid; if (!m_keys.isEmpty()) { if (m_keys.size() == 1 && m_keys.at(0) == Key(Qt::Key_G)) { // ge, gE. if (modifiers == Qt::NoModifier) { mm = Movement::BackwardEndOfWord; } else { mm = Movement::BackwardEndOfWORD; } } else { // Not a valid sequence. break; } } else { // e, E. if (modifiers == Qt::NoModifier) { mm = Movement::ForwardEndOfWord; } else { mm = Movement::ForwardEndOfWORD; } } tryAddMoveAction(); m_tokens.append(Token(mm)); processCommand(m_tokens); } break; } case Qt::Key_QuoteDbl: { if (modifiers == Qt::ShiftModifier) { // Specify a register. if (!m_keys.isEmpty() || !m_tokens.isEmpty()) { // Invalid sequence. break; } 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()) { break; } if (!hasActionToken()) { addActionToken(Action::Delete); addMovementToken(modifiers == Qt::ShiftModifier ? Movement::Left : Movement::Right); processCommand(m_tokens); } break; } break; } default: break; } clear_accept: resetState(); accept: p_event->accept(); ret = true; exit: m_resetPositionInBlock = resetPositionInBlock; return ret; } void VVim::resetState() { m_keys.clear(); m_tokens.clear(); m_register = c_unnamedRegister; m_resetPositionInBlock = true; } VimMode VVim::getMode() const { return m_mode; } void VVim::setMode(VimMode p_mode) { if (m_mode != p_mode) { clearSelection(); m_mode = p_mode; resetState(); emit modeChanged(m_mode); } } void VVim::processCommand(QList &p_tokens) { if (p_tokens.isEmpty()) { return; } V_ASSERT(p_tokens.at(0).isAction()); for (int i = 0; i < p_tokens.size(); ++i) { qDebug() << "token" << i << p_tokens[i].toString(); } Token act = p_tokens.takeFirst(); switch (act.m_action) { case Action::Move: processMoveAction(p_tokens); break; case Action::Delete: processDeleteAction(p_tokens); 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()) { return; } QTextCursor cursor = m_editor->textCursor(); 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, m_editor->document(), moveMode, mvToken.m_movement, 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 (m_mode == VimMode::VisualLine) { expandSelectionToWholeLines(cursor); } m_editor->setTextCursor(cursor); } } bool VVim::processMovement(QTextCursor &p_cursor, const QTextDocument *p_doc, QTextCursor::MoveMode &p_moveMode, Movement p_movement, int p_repeat) { bool hasMoved = false; switch (p_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; } 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 = p_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(p_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, p_doc->blockCount() - 1); p_cursor.setPosition(p_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(p_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, p_doc->blockCount() - 1); p_cursor.setPosition(p_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::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. moveCursorFirstNonSpaceCharacter(p_cursor, 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); // @p_repeat starts from 1 while block number starts from 0. QTextBlock block = p_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); } moveCursorFirstNonSpaceCharacter(p_cursor, 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); p_cursor.movePosition(QTextCursor::Start, p_moveMode, 1); moveCursorFirstNonSpaceCharacter(p_cursor, 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); p_cursor.movePosition(QTextCursor::End, p_moveMode, 1); moveCursorFirstNonSpaceCharacter(p_cursor, p_moveMode); hasMoved = true; break; } case Movement::WordForward: { // Go to the start of next word. if (p_repeat == -1) { p_repeat = 1; } p_cursor.movePosition(QTextCursor::NextWord, p_moveMode, p_repeat); hasMoved = true; break; } case Movement::WORDForward: { // Go to the start of next WORD. if (p_repeat == -1) { p_repeat = 1; } for (int i = 0; i < p_repeat; ++i) { int start, end; // [start, end] is current WORD. findCurrentWORD(p_cursor, start, end); // Move cursor to end of current WORD. p_cursor.setPosition(end, p_moveMode); // Skip spaces. moveCursorAcrossSpaces(p_cursor, p_moveMode, true); } 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(); // First move to the end of current word. p_cursor.movePosition(QTextCursor::EndOfWord, p_moveMode, 1); if (pos != p_cursor.position()) { // We did move. p_repeat -= 1; } if (p_repeat) { p_cursor.movePosition(QTextCursor::NextWord, p_moveMode, p_repeat); p_cursor.movePosition(QTextCursor::EndOfWord, 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; } for (int i = 0; i < p_repeat; ++i) { // Skip spaces. moveCursorAcrossSpaces(p_cursor, p_moveMode, true); int start, end; // [start, end] is current WORD. findCurrentWORD(p_cursor, start, end); // Move cursor to the end of current WORD. p_cursor.setPosition(end, 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, 1); if (pos != p_cursor.position()) { // We did move. p_repeat -= 1; } if (p_repeat) { p_cursor.movePosition(QTextCursor::PreviousWord, p_moveMode, p_repeat); } hasMoved = true; break; } case Movement::WORDBackward: { // Go to the start of previous WORD or current WORD. if (p_repeat == -1) { p_repeat = 1; } for (int i = 0; i < p_repeat; ++i) { // Skip Spaces. moveCursorAcrossSpaces(p_cursor, p_moveMode, false); int start, end; // [start, end] is current WORD. findCurrentWORD(p_cursor, start, end); // Move cursor to the start of current WORD. p_cursor.setPosition(start, p_moveMode); } hasMoved = true; break; } case Movement::BackwardEndOfWord: { // Go to the end of previous word. if (p_repeat == -1) { p_repeat = 1; } int pib = p_cursor.positionInBlock(); if (!(pib > 0 && p_cursor.block().text()[pib -1].isSpace())) { ++p_repeat; } p_cursor.movePosition(QTextCursor::PreviousWord, p_moveMode, p_repeat); p_cursor.movePosition(QTextCursor::EndOfWord, p_moveMode, 1); hasMoved = true; break; } case Movement::BackwardEndOfWORD: { // Go to the end of previous WORD. if (p_repeat == -1) { p_repeat = 1; } for (int i = 0; i < p_repeat; ++i) { int start, end; findCurrentWORD(p_cursor, start, end); p_cursor.setPosition(start, p_moveMode); moveCursorAcrossSpaces(p_cursor, p_moveMode, false); } hasMoved = true; 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()) { return; } QTextCursor cursor = m_editor->textCursor(); QTextDocument *doc = m_editor->document(); bool hasMoved = false; QTextCursor::MoveMode moveMode = QTextCursor::KeepAnchor; if (to.isRange()) { switch (to.m_range) { case Range::Line: { // dd, Delete current line. if (repeat == -1) { repeat = 1; } QString deletedText; cursor.beginEditBlock(); for (int i = 0; i < repeat; ++i) { QString tmp; int blockNum = cursor.block().blockNumber(); VEditUtils::removeBlock(cursor, &tmp); deletedText += tmp; if (blockNum > cursor.block().blockNumber()) { // The last block. break; } } cursor.endEditBlock(); saveToRegister(deletedText); hasMoved = true; break; } case Range::Word: break; default: return; } 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(); hasMoved = processMovement(cursor, doc, moveMode, to.m_movement, repeat); if (repeat == -1) { repeat = 1; } if (hasMoved) { bool clearEmptyBlock = false; switch (to.m_movement) { case Movement::Left: { qDebug() << "delete backward" << repeat << "chars"; break; } case Movement::Right: { qDebug() << "delete forward" << repeat << "chars"; break; } 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::VisualUp: { qDebug() << "delete visual up" << repeat << "lines"; break; } case Movement::VisualDown: { qDebug() << "delete visual down" << repeat << "lines"; break; } case Movement::StartOfLine: { qDebug() << "delete till start of line"; break; } case Movement::EndOfLine: { // End of line (block). if (repeat > 1) { clearEmptyBlock = true; } qDebug() << "delete till end of" << repeat << "line"; break; } case Movement::FirstCharacter: { qDebug() << "delete till first non-space character"; 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; } case Movement::WordForward: { qDebug() << "delete" << repeat << "words forward"; break; } case Movement::WORDForward: { qDebug() << "delete" << repeat << "WORDs forward"; break; } case Movement::ForwardEndOfWord: { qDebug() << "delete" << repeat << "end of words forward"; break; } case Movement::ForwardEndOfWORD: { qDebug() << "delete" << repeat << "end of WORDs forward"; break; } case Movement::WordBackward: { qDebug() << "delete" << repeat << "words backward"; break; } case Movement::WORDBackward: { qDebug() << "delete" << repeat << "WORDs backward"; break; } case Movement::BackwardEndOfWord: { qDebug() << "delete" << repeat << "end of words backward"; break; } case Movement::BackwardEndOfWORD: { qDebug() << "delete" << repeat << "end of WORDs backward"; break; } default: break; } deleteSelectedText(cursor, clearEmptyBlock); } cursor.endEditBlock(); exit: if (hasMoved) { m_editor->setTextCursor(cursor); } } bool VVim::clearSelection() { QTextCursor cursor = m_editor->textCursor(); if (cursor.hasSelection()) { cursor.clearSelection(); m_editor->setTextCursor(cursor); return true; } return false; } int VVim::blockCountOfPageStep() const { int lineCount = m_editor->document()->blockCount(); QScrollBar *bar = m_editor->verticalScrollBar(); int steps = (bar->maximum() - bar->minimum() + bar->pageStep()); int pageLineCount = lineCount * (bar->pageStep() * 1.0 / steps); return pageLineCount; } void VVim::selectionToVisualMode(bool p_hasText) { if (p_hasText && m_mode == VimMode::Normal) { // Enter visual mode. setMode(VimMode::Visual); } } void VVim::expandSelectionToWholeLines(QTextCursor &p_cursor) { QTextDocument *doc = m_editor->document(); 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() { m_registers.clear(); for (char ch = 'a'; ch > 'z'; ++ch) { m_registers[QChar(ch)] = Register(QChar(ch)); } m_registers[c_unnamedRegister] = Register(c_unnamedRegister); m_registers[c_blackHoleRegister] = Register(c_blackHoleRegister); m_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); } 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 { for (auto const &token : m_tokens) { if (token.isAction()) { return true; } } return false; } 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()); for (auto const &token : m_tokens) { if (token.isAction()) { 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::deleteSelectedText(bool p_clearEmptyBlock) { QTextCursor cursor = m_editor->textCursor(); if (cursor.hasSelection()) { cursor.beginEditBlock(); deleteSelectedText(cursor, p_clearEmptyBlock); cursor.endEditBlock(); m_editor->setTextCursor(cursor); } } void VVim::deleteSelectedText(QTextCursor &p_cursor, bool p_clearEmptyBlock) { if (p_cursor.hasSelection()) { QString deletedText = p_cursor.selectedText(); p_cursor.removeSelectedText(); if (p_clearEmptyBlock && p_cursor.block().length() == 1) { deletedText += "\n"; VEditUtils::removeBlock(p_cursor); } saveToRegister(deletedText); } } void VVim::saveToRegister(const QString &p_text) { qDebug() << QString("save text(%1) to register(%2)").arg(p_text).arg(m_register); Register ® = m_registers[m_register]; if (reg.isNamedRegister() && reg.m_append) { // Append to current register. reg.m_value += p_text; } else { reg.m_value = p_text; } if (!reg.isBlackHoleRegister() && !reg.isUnnamedRegister()) { // Save it to unnamed register. m_registers[c_unnamedRegister].m_value = reg.m_value; } }