vnote/src/utils/vvim.cpp

6459 lines
183 KiB
C++

#include "vvim.h"
#include <QKeyEvent>
#include <QTextBlock>
#include <QTextDocument>
#include <QString>
#include <QScrollBar>
#include <QDebug>
#include <QClipboard>
#include <QApplication>
#include <QMimeData>
#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<QChar, VVim::Register> 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<VMdEditor *>(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<Token> &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<Key> &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<Key> &p_keys, QList<Token> &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<Token> &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<QPair<QChar, QChar>> pairs;
pairs.append(QPair<QChar, QChar>('(', ')'));
pairs.append(QPair<QChar, QChar>('[', ']'));
pairs.append(QPair<QChar, QChar>('{', '}'));
// Find forward for a pair (), [], and {}.
QList<QChar> 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<Token> &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<Token> &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<Token> &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 &reg = 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<Token> &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<Token> &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<Token> &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<Token> &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<Token> &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<Token> &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<Token> &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<Token> &p_tokens)
{
int repeat = 1;
QChar replaceChar;
Q_ASSERT(!p_tokens.isEmpty());
Token to = p_tokens.takeFirst();
if (to.isRepeat()) {
repeat = to.m_repeat;
Q_ASSERT(!p_tokens.isEmpty());
to = p_tokens.takeFirst();
}
Q_ASSERT(to.isKey() && p_tokens.isEmpty());
replaceChar = keyToChar(to.m_key.m_key, to.m_key.m_modifiers);
if (replaceChar.isNull()) {
return;
}
if (!(checkMode(VimMode::Normal)
|| checkMode(VimMode::Visual)
|| checkMode(VimMode::VisualLine))) {
return;
}
// Replace the next repeat characters with replaceChar until the end of line.
// If repeat is greater than the number of left characters in current line,
// do nothing.
// In visual mode, repeat is ignored.
QTextCursor cursor = m_editor->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<Token> &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<Token> &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 &reg = 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<QChar, VVim::Register> &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<Key> &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<Key> replaySeq;
if (p_key == Key(Qt::Key_Y)) {
// <leader>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)) {
// <leader>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)) {
// <leader>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)) {
// <leader>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)) {
// <leader><space>, clear search highlight
clearSearchHighlight();
} else if (p_key == Key(Qt::Key_W)) {
// <leader>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<QChar, VVim::Mark> &VVim::Marks::getMarks() const
{
return m_marks;
}
QChar VVim::Marks::getLastUsedMark() const
{
return m_lastUsedMark;
}
void VVim::processTitleJump(const QList<Token> &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);
}