Browse Source

vim-mode: support Visual mode with block cursor

Le Tan 8 years ago
parent
commit
f429ffe3e5
10 changed files with 245 additions and 71 deletions
  1. 163 21
      src/utils/vvim.cpp
  2. 20 4
      src/utils/vvim.h
  3. 10 0
      src/vconstants.h
  4. 9 6
      src/veditor.h
  5. 2 2
      src/vmdeditor.cpp
  6. 6 8
      src/vmdeditor.h
  7. 13 18
      src/vtextdocumentlayout.cpp
  8. 12 4
      src/vtextdocumentlayout.h
  9. 7 6
      src/vtextedit.cpp
  10. 3 2
      src/vtextedit.h

+ 163 - 21
src/utils/vvim.cpp

@@ -106,7 +106,8 @@ VVim::VVim(VEditor *p_editor)
       m_leaderKey(Key(Qt::Key_Space)),
       m_replayLeaderSequence(false),
       m_registerPending(false),
-      m_insertModeAfterCommand(false)
+      m_insertModeAfterCommand(false),
+      m_positionBeforeVisualMode(0)
 {
     Q_ASSERT(m_editConfig->m_enableVimMode);
 
@@ -114,8 +115,10 @@ VVim::VVim(VEditor *p_editor)
 
     initRegisters();
 
-    connect(m_editor->object(), &VEditorObject::selectionChangedByMouse,
-            this, &VVim::selectionToVisualMode);
+    connect(m_editor->object(), &VEditorObject::mousePressed,
+            this, &VVim::handleMousePressed);
+    connect(m_editor->object(), &VEditorObject::mouseMoved,
+            this, &VVim::handleMouseMoved);
 }
 
 // Set @p_cursor's position specified by @p_positionInBlock.
@@ -1332,26 +1335,33 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
     case Qt::Key_Escape:
     {
         // Clear selection and enter normal mode.
+        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);
+        setMode(VimMode::Normal, true, position);
         break;
     }
 
     case Qt::Key_V:
     {
         if (modifiers == Qt::NoModifier) {
-            // Toggle Visual Mode.
-            clearSelection();
-            VimMode mode = VimMode::Visual;
-            if (m_mode == VimMode::Visual) {
-                mode = VimMode::Normal;
+            if (checkMode(VimMode::Visual)) {
+                setMode(VimMode::Normal, true);
+            } else {
+                // Toggle Visual Mode.
+                setMode(VimMode::Visual);
+                maintainSelectionInVisualMode();
             }
-
-            setMode(mode);
         } else if (modifiers == Qt::ShiftModifier) {
             // Visual Line Mode.
             clearSelection();
@@ -2224,9 +2234,18 @@ VimMode VVim::getMode() const
     return m_mode;
 }
 
-void VVim::setMode(VimMode p_mode, bool p_clearSelection)
+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
+            && m_mode == VimMode::Visual
+            && p_mode == VimMode::Normal
+            && cursor.position() > cursor.anchor()) {
+            position = cursor.position() - 1;
+        }
+
         if (p_clearSelection) {
             clearSelection();
         }
@@ -2240,7 +2259,26 @@ void VVim::setMode(VimMode p_mode, bool p_clearSelection)
         m_mode = p_mode;
         resetState();
 
-        m_editor->setCursorBlockEnabled(checkMode(VimMode::Normal));
+        switch (m_mode) {
+        case VimMode::Insert:
+            m_editor->setCursorBlockModeW(CursorBlock::None);
+            break;
+
+        case VimMode::Visual:
+            m_positionBeforeVisualMode = cursor.anchor();
+            V_FALLTHROUGH;
+
+        default:
+            m_editor->setCursorBlockModeW(CursorBlock::RightSide);
+            break;
+        }
+
+        if (position != -1) {
+            cursor.setPosition(position);
+            m_editor->setTextCursorW(cursor);
+        }
+
+        amendCursorPosition();
 
         emit modeChanged(m_mode);
         emit vimStatusUpdated(this);
@@ -2436,8 +2474,10 @@ void VVim::processMoveAction(QList<Token> &p_tokens)
             break;
         }
 
-        if (m_mode == VimMode::VisualLine) {
+        if (checkMode(VimMode::VisualLine)) {
             expandSelectionToWholeLines(cursor);
+        } else if (checkMode(VimMode::Visual)) {
+            maintainSelectionInVisualMode(&cursor);
         }
 
         m_editor->setTextCursorW(cursor);
@@ -4848,15 +4888,58 @@ int VVim::blockCountOfPageStep() const
     return pageLineCount;
 }
 
-void VVim::selectionToVisualMode(bool p_hasText)
+void VVim::maintainSelectionInVisualMode(QTextCursor *p_cursor)
 {
-    if (p_hasText) {
-        if (m_mode == VimMode::Normal) {
-            // Enter visual mode without clearing the selection.
-            setMode(VimMode::Visual, false);
+    // 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;
+        }
+
+        m_editor->setCursorBlockModeW(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;
+
+            m_editor->setCursorBlockModeW(CursorBlock::RightSide);
+        } else {
+            cursor->setPosition(m_positionBeforeVisualMode);
+            cursor->setPosition(pos, QTextCursor::KeepAnchor);
+            hasChanged = true;
+
+            m_editor->setCursorBlockModeW(CursorBlock::LeftSide);
         }
-    } else if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) {
-        setMode(VimMode::Normal);
+    } else {
+        // Re-select.
+        if (anchor <= m_positionBeforeVisualMode) {
+            cursor->setPosition(m_positionBeforeVisualMode + 1);
+            cursor->setPosition(pos, QTextCursor::KeepAnchor);
+            hasChanged = true;
+        }
+
+        m_editor->setCursorBlockModeW(CursorBlock::RightSide);
+    }
+
+    if (hasChanged && !p_cursor) {
+        m_editor->setTextCursorW(*cursor);
     }
 }
 
@@ -5828,3 +5911,62 @@ QString VVim::readRegister(int p_key, int p_modifiers)
 
     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);
+            qDebug() << "vvim alter the cursor position one character left";
+        }
+    }
+}
+
+void VVim::handleMousePressed(QMouseEvent *p_event)
+{
+    Q_UNUSED(p_event);
+    QTextCursor cursor = m_editor->textCursorW();
+    if (checkMode(VimMode::Visual) || checkMode(VimMode::VisualLine)) {
+        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();
+    }
+}

+ 20 - 4
src/utils/vvim.h

@@ -12,6 +12,7 @@ class VEditor;
 class QKeyEvent;
 class VEditConfig;
 class QKeyEvent;
+class QMouseEvent;
 
 enum class VimMode {
     Normal = 0,
@@ -163,7 +164,7 @@ public:
     VimMode getMode() const;
 
     // Set current mode.
-    void setMode(VimMode p_mode, bool p_clearSelection = true);
+    void setMode(VimMode p_mode, bool p_clearSelection = true, int p_position = -1);
 
     // Set current register.
     void setCurrentRegisterName(QChar p_reg);
@@ -218,9 +219,13 @@ signals:
     void commandLineTriggered(VVim::CommandLineType p_type);
 
 private slots:
-    // When user use mouse to select texts in Normal mode, we should change to
-    // Visual mode.
-    void selectionToVisualMode(bool p_hasText);
+    void handleMousePressed(QMouseEvent *p_event);
+
+    void handleMouseMoved(QMouseEvent *p_event);
+
+    // When we display cursor as block, it makes no sense to put cursor at the
+    // end of line.
+    void amendCursorPosition();
 
 private:
     // Struct for a key press.
@@ -805,6 +810,12 @@ private:
     Register &getRegister(QChar p_regName) const;
     void setRegister(QChar p_regName, const QString &p_val);
 
+    // May need to do these things:
+    // 1. Change the CursorBlock mode;
+    // 2. Alter the selection to assure the character in m_positionBeforeVisualMode
+    // is always selected.
+    void maintainSelectionInVisualMode(QTextCursor *p_cursor = NULL);
+
     VEditor *m_editor;
     const VEditConfig *m_editConfig;
     VimMode m_mode;
@@ -849,6 +860,11 @@ private:
     // Whether enter insert mode after a command.
     bool m_insertModeAfterCommand;
 
+    // Cursor position when entering Visual mode.
+    // After displaying cursor as block, we need to always select current character
+    // when entering Visual mode.
+    int m_positionBeforeVisualMode;
+
     static const QChar c_unnamedRegister;
     static const QChar c_blackHoleRegister;
     static const QChar c_selectionRegister;

+ 10 - 0
src/vconstants.h

@@ -118,4 +118,14 @@ enum class StartupPageType
     Invalid
 };
 
+// Cursor block mode.
+enum class CursorBlock
+{
+    None = 0,
+    // Display a cursor block on the character on the right side of the cursor.
+    RightSide,
+    // Display a cursor block on the character on the left side of the cursor.
+    LeftSide
+};
+
 #endif

+ 9 - 6
src/veditor.h

@@ -8,6 +8,7 @@
 #include <QColor>
 
 #include "veditconfig.h"
+#include "vconstants.h"
 #include "vfile.h"
 
 class QWidget;
@@ -17,6 +18,7 @@ class QTimer;
 class QLabel;
 class VVim;
 enum class VimMode;
+class QMouseEvent;
 
 
 enum class SelectionId {
@@ -144,9 +146,6 @@ public:
     // @p_modified: if true, delete the whole content and insert the new content.
     virtual void setContent(const QString &p_content, bool p_modified = false) = 0;
 
-    // Whether display cursor as block.
-    virtual void setCursorBlockEnabled(bool p_enabled) = 0;
-
     // Set the cursor block's background and foreground.
     virtual void setCursorBlockColor(const QColor &p_bg, const QColor &p_fg) = 0;
 
@@ -186,6 +185,9 @@ public:
 
     virtual void redoW() = 0;
 
+    // Whether display cursor as block.
+    virtual void setCursorBlockModeW(CursorBlock p_mode) = 0;
+
 protected:
     void init();
 
@@ -332,9 +334,6 @@ signals:
     // Request VEditTab to save this file.
     void saveNote();
 
-    // Selection changed by mouse.
-    void selectionChangedByMouse(bool p_hasSelection);
-
     // Emit when Vim status updated.
     void vimStatusUpdated(const VVim *p_vim);
 
@@ -344,6 +343,10 @@ signals:
     // Request the edit tab to close find and replace dialog.
     void requestCloseFindReplaceDialog();
 
+    void mouseMoved(QMouseEvent *p_event);
+
+    void mousePressed(QMouseEvent *p_event);
+
 private slots:
     // Timer for find-wrap label.
     void labelTimerTimeout()

+ 2 - 2
src/vmdeditor.cpp

@@ -302,7 +302,7 @@ void VMdEditor::mousePressEvent(QMouseEvent *p_event)
 
     VTextEdit::mousePressEvent(p_event);
 
-    emit m_object->selectionChangedByMouse(textCursor().hasSelection());
+    emit m_object->mousePressed(p_event);
 }
 
 void VMdEditor::mouseReleaseEvent(QMouseEvent *p_event)
@@ -322,7 +322,7 @@ void VMdEditor::mouseMoveEvent(QMouseEvent *p_event)
 
     VTextEdit::mouseMoveEvent(p_event);
 
-    emit m_object->selectionChangedByMouse(textCursor().hasSelection());
+    emit m_object->mouseMoved(p_event);
 }
 
 QVariant VMdEditor::inputMethodQuery(Qt::InputMethodQuery p_query) const

+ 6 - 8
src/vmdeditor.h

@@ -59,9 +59,6 @@ public:
 
     void setContent(const QString &p_content, bool p_modified = false) Q_DECL_OVERRIDE;
 
-    // Whether display cursor as block.
-    void setCursorBlockEnabled(bool p_enabled) Q_DECL_OVERRIDE;
-
     // Set the cursor block's background and foreground.
     void setCursorBlockColor(const QColor &p_bg, const QColor &p_fg) Q_DECL_OVERRIDE;
 
@@ -148,6 +145,12 @@ public:
         redo();
     }
 
+    // Whether display cursor as block.
+    void setCursorBlockModeW(CursorBlock p_mode) Q_DECL_OVERRIDE
+    {
+        setCursorBlockMode(p_mode);
+    }
+
 signals:
     // Signal when headers change.
     void headersChanged(const QVector<VTableOfContentItem> &p_headers);
@@ -218,11 +221,6 @@ private:
     bool m_freshEdit;
 };
 
-inline void VMdEditor::setCursorBlockEnabled(bool p_enabled)
-{
-    setCursorBlockMode(p_enabled);
-}
-
 inline void VMdEditor::setCursorBlockColor(const QColor &p_bg, const QColor &p_fg)
 {
     setCursorBlockBg(p_bg);

+ 13 - 18
src/vtextdocumentlayout.cpp

@@ -32,7 +32,7 @@ VTextDocumentLayout::VTextDocumentLayout(QTextDocument *p_doc,
       m_blockImageEnabled(false),
       m_imageWidthConstrainted(false),
       m_imageLineColor("#9575CD"),
-      m_cursorBlockMode(false),
+      m_cursorBlockMode(CursorBlock::None),
       m_virtualCursorBlockWidth(8),
       m_cursorBlockFg("#EEEEEE"),
       m_cursorBlockBg("#222222"),
@@ -232,14 +232,17 @@ void VTextDocumentLayout::draw(QPainter *p_painter, const PaintContext &p_contex
         bool drawCursor = p_context.cursorPosition >= blpos
                           && p_context.cursorPosition < blpos + bllen;
         int cursorWidth = m_cursorWidth;
-        if (drawCursor && m_cursorBlockMode) {
-            if (p_context.cursorPosition == blpos + bllen - 1) {
+        int cursorPosition = p_context.cursorPosition - blpos;
+        if (drawCursor && m_cursorBlockMode != CursorBlock::None) {
+            if (cursorPosition > 0 && m_cursorBlockMode == CursorBlock::LeftSide) {
+                --cursorPosition;
+            }
+
+            if (cursorPosition == bllen - 1) {
                 cursorWidth = m_virtualCursorBlockWidth;
             } else {
                 // Get the width of the selection to update cursor width.
-                cursorWidth = getTextWidthWithinTextLine(layout,
-                                                         p_context.cursorPosition - blpos,
-                                                         1);
+                cursorWidth = getTextWidthWithinTextLine(layout, cursorPosition, 1);
                 if (cursorWidth < m_cursorWidth) {
                     cursorWidth = m_cursorWidth;
                 }
@@ -263,16 +266,14 @@ void VTextDocumentLayout::draw(QPainter *p_painter, const PaintContext &p_contex
         if (drawCursor
             || (p_context.cursorPosition < -1
                 && !layout->preeditAreaText().isEmpty())) {
-            int cpos = p_context.cursorPosition;
-            if (cpos < -1) {
-                cpos = layout->preeditAreaPosition() - (cpos + 2);
-            } else {
-                cpos -= blpos;
+            if (p_context.cursorPosition < -1) {
+                cursorPosition = layout->preeditAreaPosition()
+                                 - (p_context.cursorPosition + 2);
             }
 
             layout->drawCursor(p_painter,
                                offset,
-                               cpos,
+                               cursorPosition,
                                cursorWidth);
         }
 
@@ -353,12 +354,6 @@ int VTextDocumentLayout::hitTest(const QPointF &p_point, Qt::HitTestAccuracy p_a
         }
     }
 
-    if (m_cursorBlockMode
-        && off == block.length() - 1
-        && off != 0) {
-        --off;
-    }
-
     return block.position() + off;
 }
 

+ 12 - 4
src/vtextdocumentlayout.h

@@ -5,6 +5,7 @@
 #include <QVector>
 #include <QSize>
 #include <QSet>
+#include "vconstants.h"
 
 class VImageResourceManager2;
 struct VPreviewedImageInfo;
@@ -54,7 +55,7 @@ public:
 
     void setImageLineColor(const QColor &p_color);
 
-    void setCursorBlockMode(bool p_enabled);
+    void setCursorBlockMode(CursorBlock p_mode);
 
     void setCursorBlockFg(const QColor &p_color);
 
@@ -62,6 +63,8 @@ public:
 
     void setVirtualCursorBlockWidth(int p_width);
 
+    void clearLastCursorBlockWidth();
+
 signals:
     // Emit to update current cursor block width if m_cursorBlockMode is enabled.
     void cursorBlockWidthUpdated(int p_width);
@@ -282,7 +285,7 @@ private:
     QColor m_imageLineColor;
 
     // Draw cursor as block.
-    bool m_cursorBlockMode;
+    CursorBlock m_cursorBlockMode;
 
     // Virtual cursor block: cursor block on no character.
     int m_virtualCursorBlockWidth;
@@ -313,9 +316,9 @@ inline void VTextDocumentLayout::scaleSize(QSize &p_size, int p_width, int p_hei
     }
 }
 
-inline void VTextDocumentLayout::setCursorBlockMode(bool p_enabled)
+inline void VTextDocumentLayout::setCursorBlockMode(CursorBlock p_mode)
 {
-    m_cursorBlockMode = p_enabled;
+    m_cursorBlockMode = p_mode;
 }
 
 inline void VTextDocumentLayout::setCursorBlockFg(const QColor &p_color)
@@ -332,4 +335,9 @@ inline void VTextDocumentLayout::setVirtualCursorBlockWidth(int p_width)
 {
     m_virtualCursorBlockWidth = p_width;
 }
+
+inline void VTextDocumentLayout::clearLastCursorBlockWidth()
+{
+    m_lastCursorBlockWidth = -1;
+}
 #endif // VTEXTDOCUMENTLAYOUT_H

+ 7 - 6
src/vtextedit.cpp

@@ -49,7 +49,7 @@ void VTextEdit::init()
 
     m_blockImageEnabled = false;
 
-    m_cursorBlockMode = false;
+    m_cursorBlockMode = CursorBlock::None;
 
     m_imageMgr = new VImageResourceManager2();
 
@@ -346,13 +346,14 @@ void VTextEdit::setImageLineColor(const QColor &p_color)
     getLayout()->setImageLineColor(p_color);
 }
 
-void VTextEdit::setCursorBlockMode(bool p_enabled)
+void VTextEdit::setCursorBlockMode(CursorBlock p_mode)
 {
-    if (p_enabled != m_cursorBlockMode) {
-        m_cursorBlockMode = p_enabled;
+    if (p_mode != m_cursorBlockMode) {
+        m_cursorBlockMode = p_mode;
         getLayout()->setCursorBlockMode(m_cursorBlockMode);
-
-        setCursorWidth(m_cursorBlockMode ? VIRTUAL_CURSOR_BLOCK_WIDTH : 1);
+        getLayout()->clearLastCursorBlockWidth();
+        setCursorWidth(m_cursorBlockMode != CursorBlock::None ? VIRTUAL_CURSOR_BLOCK_WIDTH
+                                                              : 1);
     }
 }
 

+ 3 - 2
src/vtextedit.h

@@ -5,6 +5,7 @@
 #include <QTextBlock>
 
 #include "vlinenumberarea.h"
+#include "vconstants.h"
 
 class VTextDocumentLayout;
 class QPainter;
@@ -56,7 +57,7 @@ public:
 
     void relayout(const QSet<int> &p_blocks);
 
-    void setCursorBlockMode(bool p_enabled);
+    void setCursorBlockMode(CursorBlock p_mode);
 
     void setCursorBlockFg(const QColor &p_color);
 
@@ -85,7 +86,7 @@ private:
 
     bool m_blockImageEnabled;
 
-    bool m_cursorBlockMode;
+    CursorBlock m_cursorBlockMode;
 };
 
 inline void VTextEdit::setLineNumberType(LineNumberType p_type)