vim-mode: support r for replacement

This commit is contained in:
Le Tan 2017-06-30 19:17:42 +08:00
parent 6df4dbe12a
commit e1acd6e9a2
4 changed files with 143 additions and 9 deletions

View File

@ -120,14 +120,18 @@ VNote supports a simple but useful Vim mode, including **Normal**, **Insert**, *
VNote supports following features of Vim:
- `d`, `c`, `y`, `p`, `<`, `>`, `gu`, `gU` actions;
- `r`, `s`, `i`, `I`, `a`, `A`, `o`, and `O`;
- Actions `d`, `c`, `y`, `p`, `<`, `>`, `gu`, and `gU`;
- Movements `h/j/k/l`, `gj/gk`, `Ctrl+U`, `Ctrl+D`, `gg`, `G`, `0`, `^`, and `$`;
- Marks `a-z`;
- Registers `"`, `_`, `+`, `a-z`(`A-Z`);
- Jump locations list (`Ctrl+O` and `Ctrl+I`);
- Leader key (`Space`)
- Currently `<leader>y/d/p` equals to `"+y/d/p`, which will access the system's clipboard.
- Currently `<leader>y/d/p` equals to `"+y/d/p`, which will access the system's clipboard;
- `zz`, `zb`, `zt`;
- `u` and `Ctrl+R` for undo and redo;
- Text objects `i/a`: word, WORD, `''`, `""`, `` ` ` ``, `()`, `[]`, `<>`, and `{}`;
- Command line `:w`, `:wq`, `:x`, `:q`, and `:q!`;
For now, VNote does **NOT** support the macro and repeat(`.`) features of Vim.

View File

@ -120,14 +120,17 @@ VNote支持一个简单但有用的Vim模式包括 **正常** **插入**
VNote支持以下几个Vim的特性
- `d`, `c`, `y`, `p`, `<`, `>`, `gu`, `gU` 操作;
- `r`, `s`, `i`, `I`, `a`, `A`, `o`, `O`;
- 操作 `d`, `c`, `y`, `p`, `<`, `>`, `gu`, `gU`
- 标记 `a-z`
- 寄存器 `"`, `_`, `+`, `a-z`(`A-Z`)
- 跳转位置列表 (`Ctrl+O` and `Ctrl+I`)
- 前导键 (`Space`)
- 目前 `<leader>y/d/p` 等同于 `"+y/d/p`, 从而可以访问系统剪切板
- 目前 `<leader>y/d/p` 等同于 `"+y/d/p`, 从而可以访问系统剪切板
- `zz`, `zb`, `zt`;
- `u``Ctrl+R` 撤销和重做;
- 文本对象 `i/a`word, WORD, `''`, `""`, `` ` ` ``, `()`, `[]`, `<>`, and `{}`;
- 命令行 `:w`, `:wq`, `:x`, `:q`, and `:q!`;
VNote目前暂时不支持Vim的宏和重复(`.`)特性。

View File

@ -351,6 +351,31 @@ static bool isControlModifier(int p_modifiers)
#endif
}
// Replace each of the character of selected text with @p_char.
// Returns true if replacement has taken place.
// Need to setTextCursor() after calling this.
static bool replaceSelectedTextWithCharacter(QTextCursor &p_cursor, QChar p_char)
{
if (!p_cursor.hasSelection()) {
return false;
}
int start = p_cursor.selectionStart();
int end = p_cursor.selectionEnd();
p_cursor.setPosition(start, QTextCursor::MoveAnchor);
while (p_cursor.position() < end) {
if (p_cursor.atBlockEnd()) {
p_cursor.movePosition(QTextCursor::NextCharacter);
} else {
p_cursor.deleteChar();
// insertText() will move the cursor right after the inserted text.
p_cursor.insertText(p_char);
}
}
return true;
}
bool VVim::handleKeyPressEvent(QKeyEvent *p_event, int *p_autoIndentPos)
{
bool ret = handleKeyPressEvent(p_event->key(), p_event->modifiers(), p_autoIndentPos);
@ -511,6 +536,15 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
goto clear_accept;
}
if (expectingReplaceCharacter()) {
// Expecting a character to replace with for r.
addActionToken(Action::Replace);
addKeyToken(keyInfo);
processCommand(m_tokens);
goto clear_accept;
}
// Check leader key here. If leader key conflicts with other keys, it will
// overwrite it.
// Leader sequence is just like an action.
@ -1632,6 +1666,13 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
addActionToken(Action::Redo);
processCommand(m_tokens);
break;
} else if (modifiers == Qt::NoModifier) {
// r, replace.
tryGetRepeatToken(m_keys, m_tokens);
if (m_keys.isEmpty() && !hasActionToken()) {
m_keys.append(keyInfo);
goto accept;
}
}
break;
@ -1974,6 +2015,10 @@ void VVim::processCommand(QList<Token> &p_tokens)
processJumpLocationAction(p_tokens, true);
break;
case Action::Replace:
processReplaceAction(p_tokens);
break;
default:
p_tokens.clear();
break;
@ -3971,6 +4016,55 @@ void VVim::processJumpLocationAction(QList<Token> &p_tokens, bool p_next)
}
}
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->textCursor();
cursor.beginEditBlock();
if (checkMode(VimMode::Normal)) {
// Select the characters to be replaced.
cursor.clearSelection();
int pib = cursor.positionInBlock();
int nrChar = cursor.block().length() - 1 - pib;
if (repeat <= nrChar) {
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, repeat);
}
}
bool changed = replaceSelectedTextWithCharacter(cursor, replaceChar);
cursor.endEditBlock();
if (changed) {
m_editor->setTextCursor(cursor);
setMode(VimMode::Normal);
}
}
bool VVim::clearSelection()
{
QTextCursor cursor = m_editor->textCursor();
@ -4051,6 +4145,12 @@ bool VVim::expectingCharacterTarget() const
|| key == Key(Qt::Key_T, Qt::ShiftModifier));
}
bool VVim::expectingReplaceCharacter() const
{
return m_keys.size() == 1
&& m_keys.first() == Key(Qt::Key_R, Qt::NoModifier);
}
bool VVim::expectingCommandLineInput() const
{
return m_cmdMode;
@ -4227,6 +4327,11 @@ void VVim::addMovementToken(Movement p_movement, Key p_key)
m_tokens.append(Token(p_movement, p_key));
}
void VVim::addKeyToken(Key p_key)
{
m_tokens.append(Token(p_key));
}
void VVim::deleteSelectedText(QTextCursor &p_cursor, bool p_clearEmptyBlock)
{
if (p_cursor.hasSelection()) {

View File

@ -262,6 +262,7 @@ private:
RedrawAtBottom,
JumpPreviousLocation,
JumpNextLocation,
Replace,
Invalid
};
@ -326,7 +327,7 @@ private:
Invalid
};
enum class TokenType { Action = 0, Repeat, Movement, Range, Invalid };
enum class TokenType { Action = 0, Repeat, Movement, Range, Key, Invalid };
struct Token
{
@ -345,6 +346,9 @@ private:
Token(Range p_range)
: m_type(TokenType::Range), m_range(p_range) {}
Token(Key p_key)
: m_type(TokenType::Key), m_key(p_key) {}
Token() : m_type(TokenType::Invalid) {}
bool isRepeat() const
@ -367,6 +371,11 @@ private:
return m_type == TokenType::Range;
}
bool isKey() const
{
return m_type == TokenType::Key;
}
bool isValid() const
{
return m_type != TokenType::Invalid;
@ -392,6 +401,10 @@ private:
str = QString("range %1").arg((int)m_range);
break;
case TokenType::Key:
str = QString("key %1 %2").arg(m_key.m_key).arg(m_key.m_modifiers);
break;
default:
str = "invalid";
}
@ -409,7 +422,7 @@ private:
Movement m_movement;
};
// Used in some Movement.
// Used in some Movement and Key Token.
Key m_key;
};
@ -503,6 +516,9 @@ private:
// Action::JumpPreviousLocation and Action::JumpNextLocation action.
void processJumpLocationAction(QList<Token> &p_tokens, bool p_next);
// Action::Replace.
void processReplaceAction(QList<Token> &p_tokens);
// Clear selection if there is any.
// Returns true if there is selection.
bool clearSelection();
@ -525,6 +541,9 @@ private:
// Check m_keys to see if we are expecting a target for f/t/F/T command.
bool expectingCharacterTarget() const;
// Check m_keys to see if we are expecting a character to replace with.
bool expectingReplaceCharacter() const;
// Check if we are in command line mode.
bool expectingCommandLineInput() const;
@ -560,15 +579,18 @@ private:
// Get the repeat token from m_tokens.
Token *getRepeatToken();
// Add an Range token at the end of m_tokens.
// Add a Range token at the end of m_tokens.
void addRangeToken(Range p_range);
// Add an Movement token at the end of m_tokens.
// Add a Movement token at the end of m_tokens.
void addMovementToken(Movement p_movement);
// Add an Movement token at the end of m_tokens.
// Add a Movement token at the end of m_tokens.
void addMovementToken(Movement p_movement, Key p_key);
// Add a Key token at the end of m_tokens.
void addKeyToken(Key p_key);
// Delete selected text if there is any.
// @p_clearEmptyBlock: whether to remove the empty block after deletion.
void deleteSelectedText(QTextCursor &p_cursor, bool p_clearEmptyBlock);