vim-mode: support undo/redo and zt/zz/zb

Change SaveAndRead from Ctrl+R to Ctrl+T.
This commit is contained in:
Le Tan 2017-06-19 21:39:22 +08:00
parent d909091f46
commit 5953954786
5 changed files with 281 additions and 7 deletions

View File

@ -2,6 +2,8 @@
#include <QTextDocument>
#include <QDebug>
#include <QTextEdit>
#include <QScrollBar>
#include "vutils.h"
@ -264,3 +266,89 @@ int VEditUtils::selectedBlockCount(const QTextCursor &p_cursor)
return ebNum - sbNum + 1;
}
void VEditUtils::scrollBlockInPage(QTextEdit *p_edit,
int p_blockNum,
int p_dest)
{
QTextDocument *doc = p_edit->document();
QTextCursor cursor = p_edit->textCursor();
if (p_blockNum >= doc->blockCount()) {
p_blockNum = doc->blockCount() - 1;
}
QTextBlock block = doc->findBlockByNumber(p_blockNum);
int pib = cursor.positionInBlock();
if (cursor.block().blockNumber() != p_blockNum) {
// Move the cursor to the block.
if (pib >= block.length()) {
pib = block.length() - 1;
}
cursor.setPosition(block.position() + pib);
p_edit->setTextCursor(cursor);
}
// Scroll to let current cursor locate in proper position.
p_edit->ensureCursorVisible();
QScrollBar *vsbar = p_edit->verticalScrollBar();
if (!vsbar || !vsbar->isVisible()) {
// No vertical scrollbar. No need to scrool.
return;
}
QRect rect = p_edit->cursorRect();
int height = p_edit->rect().height();
QScrollBar *sbar = p_edit->horizontalScrollBar();
if (sbar && sbar->isVisible()) {
height -= sbar->height();
}
switch (p_dest) {
case 0:
{
// Top.
while (rect.y() > 0 && vsbar->value() < vsbar->maximum()) {
vsbar->setValue(vsbar->value() + vsbar->singleStep());
rect = p_edit->cursorRect();
}
break;
}
case 1:
{
// Center.
height = qMax(height / 2, 1);
if (rect.y() > height) {
while (rect.y() > height && vsbar->value() < vsbar->maximum()) {
vsbar->setValue(vsbar->value() + vsbar->singleStep());
rect = p_edit->cursorRect();
}
} else if (rect.y() < height) {
while (rect.y() < height && vsbar->value() > vsbar->minimum()) {
vsbar->setValue(vsbar->value() - vsbar->singleStep());
rect = p_edit->cursorRect();
}
}
break;
}
case 2:
// Bottom.
while (rect.y() < height && vsbar->value() > vsbar->minimum()) {
vsbar->setValue(vsbar->value() - vsbar->singleStep());
rect = p_edit->cursorRect();
}
break;
default:
break;
}
p_edit->ensureCursorVisible();
}

View File

@ -5,6 +5,7 @@
#include <QTextCursor>
class QTextDocument;
class QTextEdit;
// Utils for text edit.
class VEditUtils
@ -74,6 +75,13 @@ public:
// Get the count of blocks selected.
static int selectedBlockCount(const QTextCursor &p_cursor);
// Scroll block @p_blockNum into the visual window.
// @p_dest is the position of the window: 0 for top, 1 for center, 2 for bottom.
// @p_blockNum is based on 0.
static void scrollBlockInPage(QTextEdit *p_edit,
int p_blockNum,
int p_dest);
private:
VEditUtils() {}
};

View File

@ -670,7 +670,13 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
} else if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) {
tryGetRepeatToken(m_keys, m_tokens);
if (!m_keys.isEmpty()) {
// Not a valid sequence.
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;
}
@ -711,9 +717,11 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
case Qt::Key_U:
{
tryGetRepeatToken(m_keys, m_tokens);
bool toLower = modifiers == Qt::NoModifier;
if (modifiers == Qt::ControlModifier) {
// Ctrl+U, HalfPageUp.
tryGetRepeatToken(m_keys, m_tokens);
if (!m_keys.isEmpty()) {
// Not a valid sequence.
break;
@ -724,12 +732,34 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
m_tokens.append(Token(mm));
processCommand(m_tokens);
resetPositionInBlock = false;
} else if (m_keys.isEmpty() && m_tokens.isEmpty() && modifiers == Qt::NoModifier) {
} 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->textCursor();
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->setTextCursor(cursor);
setMode(VimMode::Normal);
break;
}
// u, Undo.
if (modifiers == Qt::NoModifier) {
addActionToken(Action::Undo);
processCommand(m_tokens);
}
break;
} else {
bool toLower = modifiers == Qt::NoModifier;
tryGetRepeatToken(m_keys, m_tokens);
if (hasActionToken()) {
// guu/gUU.
if ((toLower && checkActionToken(Action::ToLower))
@ -748,6 +778,12 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
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->setTextCursor(cursor);
@ -1230,6 +1266,11 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
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;
@ -1272,6 +1313,48 @@ bool VVim::handleKeyPressEvent(QKeyEvent *p_event)
break;
}
case Qt::Key_R:
{
if (m_mode == VimMode::VisualLine) {
break;
}
if (modifiers == Qt::ControlModifier) {
// Redo.
tryGetRepeatToken(m_keys, m_tokens);
if (!m_keys.isEmpty() || hasActionToken()) {
break;
}
addActionToken(Action::Redo);
processCommand(m_tokens);
break;
}
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;
}
default:
break;
}
@ -1369,6 +1452,26 @@ void VVim::processCommand(QList<Token> &p_tokens)
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;
default:
p_tokens.clear();
break;
@ -3003,6 +3106,67 @@ exit:
}
}
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->document();
int i = 0;
for (i = 0; i < repeat && doc->isUndoAvailable(); ++i) {
doc->undo();
}
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->document();
int i = 0;
for (i = 0; i < repeat && doc->isRedoAvailable(); ++i) {
doc->redo();
}
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->textCursor();
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;
}
VEditUtils::scrollBlockInPage(m_editor, repeat, p_dest);
}
bool VVim::clearSelection()
{
QTextCursor cursor = m_editor->textCursor();

View File

@ -196,6 +196,11 @@ private:
UnIndent,
ToUpper,
ToLower,
Undo,
Redo,
RedrawAtTop,
RedrawAtCenter,
RedrawAtBottom,
Invalid
};
@ -379,6 +384,16 @@ private:
// @p_tokens is the arguments of the Action::ToLower and Action::ToUpper action.
void processToLowerAction(QList<Token> &p_tokens, bool p_toLower);
// @p_tokens is the arguments of the Action::Undo action.
void processUndoAction(QList<Token> &p_tokens);
// @p_tokens is the arguments of the Action::Redo action.
void processRedoAction(QList<Token> &p_tokens);
// @p_tokens is the arguments of the Action::RedrawAtBottom/RedrawAtCenter/RedrawAtTop action.
// @p_dest: 0 for top, 1 for center, 2 for bottom.
void processRedrawLineAction(QList<Token> &p_tokens, int p_dest);
// Clear selection if there is any.
// Returns true if there is selection.
bool clearSelection();

View File

@ -196,7 +196,6 @@ void VMainWindow::initViewToolBar()
tr("Expand"), this);
expandViewAct->setStatusTip(tr("Expand the edit area"));
expandViewAct->setCheckable(true);
expandViewAct->setShortcut(QKeySequence("Ctrl+T"));
expandViewAct->setMenu(panelMenu);
connect(expandViewAct, &QAction::triggered,
this, &VMainWindow::expandPanelView);
@ -255,7 +254,7 @@ void VMainWindow::initFileToolBar()
tr("Save Changes And Read"), this);
saveExitAct->setStatusTip(tr("Save changes and exit edit mode"));
saveExitAct->setMenu(exitEditMenu);
saveExitAct->setShortcut(QKeySequence("Ctrl+R"));
saveExitAct->setShortcut(QKeySequence("Ctrl+T"));
connect(saveExitAct, &QAction::triggered,
editArea, &VEditArea::saveAndReadFile);