Browse Source

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

Change SaveAndRead from Ctrl+R to Ctrl+T.
Le Tan 8 years ago
parent
commit
5953954786
5 changed files with 281 additions and 7 deletions
  1. 88 0
      src/utils/veditutils.cpp
  2. 8 0
      src/utils/veditutils.h
  3. 169 5
      src/utils/vvim.cpp
  4. 15 0
      src/utils/vvim.h
  5. 1 2
      src/vmainwindow.cpp

+ 88 - 0
src/utils/veditutils.cpp

@@ -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();
+}

+ 8 - 0
src/utils/veditutils.h

@@ -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() {}
 };

+ 169 - 5
src/utils/vvim.cpp

@@ -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();

+ 15 - 0
src/utils/vvim.h

@@ -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();

+ 1 - 2
src/vmainwindow.cpp

@@ -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);