From 0f25f0c89b4b241e09e83eac93b67e14757b6d40 Mon Sep 17 00:00:00 2001
From: hluk <hluk@email.cz>
Date: Sun, 17 Mar 2013 12:41:18 +0100
Subject: [PATCH] FakeVim: Initial support for Vim macro recording and
 replaying

Record macro with 'q{0-9a-zA-Z"}' command and replay it with
'@{0-9a-z".*+}'. Replaying with prompt ('=' instead of register) not
supported.

Change-Id: I49965a99d10910e8df09e62020ed496542e3b249
Reviewed-by: hjk <hjk121@nokiamail.com>
---
 src/plugins/fakevim/fakevim_test.cpp   |  37 +++++++
 src/plugins/fakevim/fakevimhandler.cpp | 127 ++++++++++++++++++++++---
 src/plugins/fakevim/fakevimplugin.cpp  |   2 +-
 src/plugins/fakevim/fakevimplugin.h    |   2 +
 4 files changed, 156 insertions(+), 12 deletions(-)

diff --git a/src/plugins/fakevim/fakevim_test.cpp b/src/plugins/fakevim/fakevim_test.cpp
index ba4ba26d84b..1997abae86c 100644
--- a/src/plugins/fakevim/fakevim_test.cpp
+++ b/src/plugins/fakevim/fakevim_test.cpp
@@ -2889,3 +2889,40 @@ void FakeVimPlugin::test_vim_Visual_d()
     KEYS("Vkx",   '|' + lmid(4));
     KEYS("P",     '|' + lmid(0,1)+'\n' + lmid(3));
 }
+
+void FakeVimPlugin::test_macros()
+{
+    TestData data;
+    setup(&data);
+
+    // execute register content
+    data.setText("r1" N "r2r3");
+    KEYS("\"xy$", X "r1" N "r2r3");
+    KEYS("@x", X "11" N "r2r3");
+    INTEGRITY(false);
+
+    data.doKeys("j\"xy$");
+    KEYS("@x", "11" N X "32r3");
+    INTEGRITY(false);
+
+    data.setText("3<C-A>");
+    KEYS("\"xy$", X "3<C-A>");
+    KEYS("@x", X "6<C-A>");
+    KEYS("@x", X "9<C-A>");
+    KEYS("2@x", "1" X "5<C-A>");
+    KEYS("2@@", "2" X "1<C-A>");
+    KEYS("@@", "2" X "4<C-A>");
+
+// Raw characters for macro recording.
+#define ESC "\x1b"
+#define ENTER "\n"
+
+    // record
+    data.setText("abc" N "def");
+    KEYS("qx" "A" ENTER "- xyz" ESC "rZjI- opq" ENTER ESC "q" , "abc" N "- xyZ" N "- opq" N X "def");
+    KEYS("@x" , "abc" N "- xyZ" N "- opq" N "def" N "- opq" N X "- xyZ");
+
+    data.setText("  1 2 3" N "  4 5 6" N "  7 8 9");
+    KEYS("qx" "wrXj" "q", "  X 2 3" N "  4 5 6" N "  7 8 9");
+    KEYS("2@x", "  X 2 3" N "  4 X 6" N "  7 8 X");
+}
diff --git a/src/plugins/fakevim/fakevimhandler.cpp b/src/plugins/fakevim/fakevimhandler.cpp
index aead79f14c8..c4a13496d44 100644
--- a/src/plugins/fakevim/fakevimhandler.cpp
+++ b/src/plugins/fakevim/fakevimhandler.cpp
@@ -178,7 +178,9 @@ enum SubMode
     YankSubMode,         // Used for y
     ZSubMode,            // Used for z
     CapitalZSubMode,     // Used for Z
-    ReplaceSubMode      // Used for r
+    ReplaceSubMode,      // Used for r
+    MacroRecordSubMode,  // Used for q
+    MacroExecuteSubMode  // Used for @
 };
 
 /*! A \e SubSubMode is used for things that require one more data item
@@ -937,13 +939,16 @@ public:
 
     int key() const { return m_key; }
 
+    // Return raw character for macro recording or dot command.
     QChar raw() const
     {
         if (m_key == Key_Tab)
             return QLatin1Char('\t');
         if (m_key == Key_Return)
             return QLatin1Char('\n');
-        return m_key;
+        if (m_key == Key_Escape)
+            return QChar(27);
+        return m_xkey;
     }
 
     QString toString() const
@@ -1438,6 +1443,8 @@ public:
     bool handleYankSubMode(const Input &);
     bool handleZSubMode(const Input &);
     bool handleCapitalZSubMode(const Input &);
+    bool handleMacroRecordSubMode(const Input &);
+    bool handleMacroExecuteSubMode(const Input &);
 
     bool handleCount(const Input &); // Handle count for commands (return false if input isn't count).
     bool handleMovement(const Input &);
@@ -1659,6 +1666,12 @@ public:
     void ensureCursorVisible();
     void insertInInsertMode(const QString &text);
 
+    // Macro recording
+    bool startRecording(const Input &input);
+    void record(const Input &input);
+    void stopRecording();
+    bool executeRegister(int register);
+
 public:
     QTextEdit *m_textedit;
     QPlainTextEdit *m_plaintextedit;
@@ -1848,7 +1861,7 @@ public:
         GlobalData()
             : mappings(), currentMap(&mappings), inputTimer(-1), mapDepth(0),
               currentMessageLevel(MessageInfo), lastSearchForward(false), findPending(false),
-              returnToMode(CommandMode)
+              returnToMode(CommandMode), currentRegister(0), lastExecutedRegister(0)
         {
             commandBuffer.setPrompt(QLatin1Char(':'));
         }
@@ -1892,6 +1905,11 @@ public:
 
         // Return to insert/replace mode after single command (<C-O>).
         Mode returnToMode;
+
+        // Currently recorded macro (not recording if null string).
+        QString recording;
+        int currentRegister;
+        int lastExecutedRegister;
     } g;
 };
 
@@ -2316,8 +2334,10 @@ EventResult FakeVimHandler::Private::handleKey(const Input &input)
     // Waiting on input to complete mapping?
     EventResult r = stopWaitForMapping(hasInput);
 
-    if (hasInput)
+    if (hasInput) {
+        record(input);
         g.pendingInput.append(input);
+    }
 
     // Process pending input.
     // Note: Pending input is global state and can be extended by:
@@ -2973,25 +2993,28 @@ void FakeVimHandler::Private::updateMiniBuffer()
         messageLevel = MessageShowCmd;
     } else if (m_mode == CommandMode && isVisualMode()) {
         if (isVisualCharMode())
-            msg = _("VISUAL");
+            msg = _("-- VISUAL --");
         else if (isVisualLineMode())
-            msg = _("VISUAL LINE");
+            msg = _("-- VISUAL LINE --");
         else if (isVisualBlockMode())
             msg = _("VISUAL BLOCK");
     } else if (m_mode == InsertMode) {
-        msg = _("INSERT");
+        msg = _("-- INSERT --");
     } else if (m_mode == ReplaceMode) {
-        msg = _("REPLACE");
+        msg = _("-- REPLACE --");
     } else {
         QTC_CHECK(m_mode == CommandMode && m_subsubmode != SearchSubSubMode);
         if (g.returnToMode == CommandMode)
-            msg = _("COMMAND");
+            msg = _("-- COMMAND --");
         else if (g.returnToMode == InsertMode)
-            msg = _("(insert)");
+            msg = _("-- (insert) --");
         else
-            msg = _("(replace)");
+            msg = _("-- (replace) --");
     }
 
+    if (!g.recording.isNull() && msg.startsWith(_("--")))
+        msg.append(_("recording"));
+
     emit q->commandBufferChanged(msg, cursorPos, anchorPos, messageLevel, q);
 
     int linesInDoc = linesInDocument();
@@ -3467,6 +3490,10 @@ EventResult FakeVimHandler::Private::handleCommandMode(const Input &input)
         handled = handleZSubMode(input);
     } else if (m_submode == CapitalZSubMode) {
         handled = handleCapitalZSubMode(input);
+    } else if (m_submode == MacroRecordSubMode) {
+        handled = handleMacroRecordSubMode(input);
+    } else if (m_submode == MacroExecuteSubMode) {
+        handled = handleMacroExecuteSubMode(input);
     } else if (m_submode == ShiftLeftSubMode
         || m_submode == ShiftRightSubMode
         || m_submode == IndentSubMode) {
@@ -3756,6 +3783,16 @@ bool FakeVimHandler::Private::handleNoSubMode(const Input &input)
         setTargetColumn();
         setDotCommand(_("%1p"), count());
         finishMovement();
+    } else if (input.is('q')) {
+        if (g.recording.isNull()) {
+            // Recording shouldn't work in mapping or while executing register.
+            handled = g.mapStates.empty();
+            if (handled)
+                m_submode = MacroRecordSubMode;
+        } else {
+            // Stop recording.
+            stopRecording();
+        }
     } else if (input.is('r')) {
         m_submode = ReplaceSubMode;
     } else if (!isVisualMode() && input.is('R')) {
@@ -3924,6 +3961,8 @@ bool FakeVimHandler::Private::handleNoSubMode(const Input &input)
             setDotCommand(QString::fromLatin1("%1%2").arg(count()).arg(input.raw()));
             endEditBlock();
         }
+    } else if (input.is('@')) {
+        m_submode = MacroExecuteSubMode;
     } else if (input.isKey(Key_Delete)) {
         setAnchor();
         moveRight(qMin(1, rightDist()));
@@ -4161,6 +4200,24 @@ bool FakeVimHandler::Private::handleCapitalZSubMode(const Input &input)
     return handled;
 }
 
+bool FakeVimHandler::Private::handleMacroRecordSubMode(const Input &input)
+{
+    m_submode = NoSubMode;
+    return startRecording(input);
+}
+
+bool FakeVimHandler::Private::handleMacroExecuteSubMode(const Input &input)
+{
+    m_submode = NoSubMode;
+
+    bool result = true;
+    int repeat = count();
+    while (result && --repeat >= 0)
+        result = executeRegister(input.asChar().unicode());
+
+    return result;
+}
+
 EventResult FakeVimHandler::Private::handleReplaceMode(const Input &input)
 {
     bool clearLastInsertion = m_breakEditBlock;
@@ -4466,6 +4523,54 @@ void FakeVimHandler::Private::insertInInsertMode(const QString &text)
     m_ctrlVActive = false;
 }
 
+bool FakeVimHandler::Private::startRecording(const Input &input)
+{
+    QChar reg = input.asChar();
+    if (reg == QLatin1Char('"') || reg.isLetterOrNumber()) {
+        g.currentRegister = reg.unicode();
+        g.recording = QLatin1String("");
+        return true;
+    }
+
+    return false;
+}
+
+void FakeVimHandler::Private::record(const Input &input)
+{
+    if ( !g.recording.isNull() )
+        g.recording.append(input.raw());
+}
+
+void FakeVimHandler::Private::stopRecording()
+{
+    // Remove q from end (stop recording command).
+    g.recording.remove(g.recording.size() - 1, 1);
+    setRegister(g.currentRegister, g.recording, m_rangemode);
+    g.currentRegister = 0;
+    g.recording = QString();
+}
+
+bool FakeVimHandler::Private::executeRegister(int reg)
+{
+    QChar regChar(reg);
+
+    // TODO: Prompt for an expression to execute if register is '='.
+    if (reg == '@' && g.lastExecutedRegister != 0)
+        reg = g.lastExecutedRegister;
+    else if (QString::fromLatin1("\".*+").contains(regChar) || regChar.isLetterOrNumber())
+        g.lastExecutedRegister = reg;
+    else
+        return false;
+
+    // FIXME: In Vim it's possible to interrupt recursive macro with <C-c>.
+    //        One solution may be to call QApplication::processEvents() and check if <C-c> was
+    //        used when a mapping is active.
+    // According to Vim, register is executed like mapping.
+    prependMapping(Inputs(registerContents(reg)));
+
+    return true;
+}
+
 EventResult FakeVimHandler::Private::handleExMode(const Input &input)
 {
     if (input.isEscape()) {
diff --git a/src/plugins/fakevim/fakevimplugin.cpp b/src/plugins/fakevim/fakevimplugin.cpp
index f85a76c6581..91a21b0b5ec 100644
--- a/src/plugins/fakevim/fakevimplugin.cpp
+++ b/src/plugins/fakevim/fakevimplugin.cpp
@@ -151,7 +151,7 @@ public:
             hide();
         } else {
             show();
-            m_label->setText(messageLevel == MessageMode ? _("-- ") + contents + _(" --") : contents);
+            m_label->setText(contents);
 
             QString css;
             if (messageLevel == MessageError) {
diff --git a/src/plugins/fakevim/fakevimplugin.h b/src/plugins/fakevim/fakevimplugin.h
index 2ade5e54fce..9078acce4b3 100644
--- a/src/plugins/fakevim/fakevimplugin.h
+++ b/src/plugins/fakevim/fakevimplugin.h
@@ -140,6 +140,8 @@ private slots:
     void test_vim_visual_d();
     void test_vim_Visual_d();
 
+    void test_macros();
+
     // special tests
     void test_i_cw_i();
 
-- 
GitLab