Browse Source

Editor: support completion

Le Tan 7 years ago
parent
commit
10a1e9c1a8

+ 12 - 1
src/resources/docs/shortcuts_en.md

@@ -66,7 +66,18 @@ Zoom in/out the page through the mouse scroll.
 Recover the page zoom factor to 100%.
 - `Ctrl+J/K`  
 Scroll page down/up without changing cursor.
-
+- `Ctrl+N/P`  
+Activate auto-completion.
+    - `Ctrl+N/P`  
+    Navigate through the completion list and insert current completion.
+    - `Ctrl+J/K`  
+    Navigate through the completion list.
+    - `Ctrl+E`  
+    Cancel completion.
+    - `Enter`  
+    Insert current completion.
+    - `Ctrl+[` or `Escape`  
+    Finish completion.
 
 #### Text Editing
 - `Ctrl+B`  

+ 12 - 0
src/resources/docs/shortcuts_zh.md

@@ -66,6 +66,18 @@
 恢复页面大小为100%。
 - `Ctrl+J/K`  
 向下/向上滚动页面,不会改变光标。
+- `Ctrl+N/P`  
+激活自动补全。
+    - `Ctrl+N/P`  
+    浏览补全列表并插入当前补全。
+    - `Ctrl+J/K`  
+    浏览补全列表。
+    - `Ctrl+E`  
+    取消补全。
+    - `Enter`  
+    插入补全。
+    - `Ctrl+[` or `Escape`  
+    结束补全。
 
 #### 文本编辑
 - `Ctrl+B`  

+ 13 - 1
src/resources/themes/v_detorte/v_detorte.palette

@@ -9,7 +9,7 @@ mdhl_file=v_detorte.mdhl
 css_file=v_detorte.css
 codeblock_css_file=v_detorte_codeblock.css
 mermaid_css_file=v_detorte_mermaid.css
-version=7
+version=8
 
 ; This mapping will be used to translate colors when the content of HTML is copied
 ; without background. You could just specify the foreground colors mapping here.
@@ -308,6 +308,18 @@ listview_item_selected_avtive_bg=@active_bg
 listview_item_selected_inactive_fg=@inactive_fg
 listview_item_selected_inactive_bg=@inactive_bg
 
+; QAbstractItemView for TextEdit Completer.
+abstractitemview_textedit_fg=#000000
+abstractitemview_textedit_bg=#BCBCBC
+abstractitemview_textedit_item_hover_fg=@abstractitemview_textedit_fg
+abstractitemview_textedit_item_hover_bg=@master_hover_bg
+abstractitemview_textedit_item_selected_fg=@abstractitemview_textedit_fg
+abstractitemview_textedit_item_selected_bg=@master_light_bg
+abstractitemview_textedit_item_selected_avtive_fg=@abstractitemview_textedit_fg
+abstractitemview_textedit_item_selected_avtive_bg=@master_focus_bg
+abstractitemview_textedit_item_selected_inactive_fg=@inactive_fg
+abstractitemview_textedit_item_selected_inactive_bg=@inactive_bg
+
 ; Splitter.
 splitter_handle_bg=@border_bg
 splitter_handle_pressed_bg=@pressed_bg

+ 40 - 0
src/resources/themes/v_detorte/v_detorte.qss

@@ -930,6 +930,46 @@ QListView::item:disabled {
 }
 /* End QListView */
 
+/* QAbstractItemView for TextEdit Completer popup*/
+QAbstractItemView[TextEdit="true"] {
+    color: @abstractitemview_textedit_fg;
+    background: @abstractitemview_textedit_bg;
+    show-decoration-selected: 0;
+    border: none;
+    selection-background-color: transparent;
+    outline: none;
+}
+
+QAbstractItemView[TextEdit="true"]::item {
+    padding-top: 5px;
+    padding-bottom: 5px;
+}
+
+QAbstractItemView[TextEdit="true"]::item:hover {
+    color: @abstractitemview_textedit_item_hover_fg;
+    background: @abstractitemview_textedit_item_hover_bg;
+}
+
+QAbstractItemView[TextEdit="true"]::item:selected {
+    color: @abstractitemview_textedit_item_selected_fg;
+    background: @abstractitemview_textedit_item_selected_bg;
+}
+
+QAbstractItemView[TextEdit="true"]::item:selected:active {
+    color: @abstractitemview_textedit_item_selected_active_fg;
+    background: @abstractitemview_textedit_item_selected_active_bg;
+}
+
+QAbstractItemView[TextEdit="true"]::item:selected:!active {
+    color: @abstractitemview_textedit_item_selected_inactive_fg;
+    background: @abstractitemview_textedit_item_selected_inactive_bg;
+}
+
+QAbstractItemView[TextEdit="true"]::item:disabled {
+    background: transparent;
+}
+/* End QAbstractItemView */
+
 /* QSplitter */
 QSplitter#MainSplitter {
     border: none;

+ 13 - 1
src/resources/themes/v_moonlight/v_moonlight.palette

@@ -7,7 +7,7 @@ mdhl_file=v_moonlight.mdhl
 css_file=v_moonlight.css
 codeblock_css_file=v_moonlight_codeblock.css
 mermaid_css_file=v_moonlight_mermaid.css
-version=18
+version=19
 
 ; This mapping will be used to translate colors when the content of HTML is copied
 ; without background. You could just specify the foreground colors mapping here.
@@ -306,6 +306,18 @@ listview_item_selected_avtive_bg=@active_bg
 listview_item_selected_inactive_fg=@inactive_fg
 listview_item_selected_inactive_bg=@inactive_bg
 
+; QAbstractItemView for TextEdit Completer.
+abstractitemview_textedit_fg=@content_fg
+abstractitemview_textedit_bg=#323841
+abstractitemview_textedit_item_hover_fg=@master_fg
+abstractitemview_textedit_item_hover_bg=@master_hover_bg
+abstractitemview_textedit_item_selected_fg=@master_fg
+abstractitemview_textedit_item_selected_bg=@master_bg
+abstractitemview_textedit_item_selected_avtive_fg=@master_fg
+abstractitemview_textedit_item_selected_avtive_bg=@master_focus_bg
+abstractitemview_textedit_item_selected_inactive_fg=@inactive_fg
+abstractitemview_textedit_item_selected_inactive_bg=@inactive_bg
+
 ; Splitter.
 splitter_handle_bg=@border_bg
 splitter_handle_pressed_bg=@pressed_bg

+ 40 - 0
src/resources/themes/v_moonlight/v_moonlight.qss

@@ -930,6 +930,46 @@ QListView::item:disabled {
 }
 /* End QListView */
 
+/* QAbstractItemView for TextEdit Completer popup*/
+QAbstractItemView[TextEdit="true"] {
+    color: @abstractitemview_textedit_fg;
+    background: @abstractitemview_textedit_bg;
+    show-decoration-selected: 0;
+    border: none;
+    selection-background-color: transparent;
+    outline: none;
+}
+
+QAbstractItemView[TextEdit="true"]::item {
+    padding-top: 5px;
+    padding-bottom: 5px;
+}
+
+QAbstractItemView[TextEdit="true"]::item:hover {
+    color: @abstractitemview_textedit_item_hover_fg;
+    background: @abstractitemview_textedit_item_hover_bg;
+}
+
+QAbstractItemView[TextEdit="true"]::item:selected {
+    color: @abstractitemview_textedit_item_selected_fg;
+    background: @abstractitemview_textedit_item_selected_bg;
+}
+
+QAbstractItemView[TextEdit="true"]::item:selected:active {
+    color: @abstractitemview_textedit_item_selected_active_fg;
+    background: @abstractitemview_textedit_item_selected_active_bg;
+}
+
+QAbstractItemView[TextEdit="true"]::item:selected:!active {
+    color: @abstractitemview_textedit_item_selected_inactive_fg;
+    background: @abstractitemview_textedit_item_selected_inactive_bg;
+}
+
+QAbstractItemView[TextEdit="true"]::item:disabled {
+    background: transparent;
+}
+/* End QAbstractItemView */
+
 /* QSplitter */
 QSplitter#MainSplitter {
     border: none;

+ 1 - 1
src/resources/themes/v_native/v_native.palette

@@ -7,7 +7,7 @@ mdhl_file=v_native.mdhl
 css_file=v_native.css
 codeblock_css_file=v_native_codeblock.css
 mermaid_css_file=v_native_mermaid.css
-version=15
+version=16
 
 [phony]
 ; Abstract color attributes.

+ 6 - 0
src/resources/themes/v_native/v_native.qss

@@ -510,6 +510,12 @@ QListView::item {
 }
 /* End QListView */
 
+/* QAbstractItemView for TextEdit Completer popup*/
+QAbstractItemView[TextEdit="true"] {
+    border: 1px solid @border_bg;
+}
+/* End QAbstractItemView */
+
 /* QSplitter */
 QSplitter {
     border: none;

+ 13 - 1
src/resources/themes/v_pure/v_pure.palette

@@ -7,7 +7,7 @@ mdhl_file=v_pure.mdhl
 css_file=v_pure.css
 codeblock_css_file=v_pure_codeblock.css
 mermaid_css_file=v_pure_mermaid.css
-version=16
+version=17
 
 [phony]
 ; Abstract color attributes.
@@ -300,6 +300,18 @@ listview_item_selected_avtive_bg=@active_bg
 listview_item_selected_inactive_fg=@inactive_fg
 listview_item_selected_inactive_bg=@inactive_bg
 
+; QAbstractItemView for TextEdit Completer.
+abstractitemview_textedit_fg=#content_fg
+abstractitemview_textedit_bg=#DADADA
+abstractitemview_textedit_item_hover_fg=@abstractitemview_textedit_fg
+abstractitemview_textedit_item_hover_bg=@master_hover_bg
+abstractitemview_textedit_item_selected_fg=@abstractitemview_textedit_fg
+abstractitemview_textedit_item_selected_bg=@master_light_bg
+abstractitemview_textedit_item_selected_avtive_fg=@abstractitemview_textedit_fg
+abstractitemview_textedit_item_selected_avtive_bg=@master_focus_bg
+abstractitemview_textedit_item_selected_inactive_fg=@inactive_fg
+abstractitemview_textedit_item_selected_inactive_bg=@inactive_bg
+
 ; Splitter.
 splitter_handle_bg=@border_bg
 splitter_handle_pressed_bg=@pressed_bg

+ 40 - 0
src/resources/themes/v_pure/v_pure.qss

@@ -930,6 +930,46 @@ QListView::item:disabled {
 }
 /* End QListView */
 
+/* QAbstractItemView for TextEdit Completer popup*/
+QAbstractItemView[TextEdit="true"] {
+    color: @abstractitemview_textedit_fg;
+    background: @abstractitemview_textedit_bg;
+    show-decoration-selected: 0;
+    border: none;
+    selection-background-color: transparent;
+    outline: none;
+}
+
+QAbstractItemView[TextEdit="true"]::item {
+    padding-top: 5px;
+    padding-bottom: 5px;
+}
+
+QAbstractItemView[TextEdit="true"]::item:hover {
+    color: @abstractitemview_textedit_item_hover_fg;
+    background: @abstractitemview_textedit_item_hover_bg;
+}
+
+QAbstractItemView[TextEdit="true"]::item:selected {
+    color: @abstractitemview_textedit_item_selected_fg;
+    background: @abstractitemview_textedit_item_selected_bg;
+}
+
+QAbstractItemView[TextEdit="true"]::item:selected:active {
+    color: @abstractitemview_textedit_item_selected_active_fg;
+    background: @abstractitemview_textedit_item_selected_active_bg;
+}
+
+QAbstractItemView[TextEdit="true"]::item:selected:!active {
+    color: @abstractitemview_textedit_item_selected_inactive_fg;
+    background: @abstractitemview_textedit_item_selected_inactive_bg;
+}
+
+QAbstractItemView[TextEdit="true"]::item:disabled {
+    background: transparent;
+}
+/* End QAbstractItemView */
+
 /* QSplitter */
 QSplitter#MainSplitter {
     border: none;

+ 4 - 2
src/src.pro

@@ -142,7 +142,8 @@ SOURCES += main.cpp\
     vtagexplorer.cpp \
     pegmarkdownhighlighter.cpp \
     pegparser.cpp \
-    peghighlighterresult.cpp
+    peghighlighterresult.cpp \
+    vtexteditcompleter.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -279,7 +280,8 @@ HEADERS  += vmainwindow.h \
     markdownhighlighterdata.h \
     pegmarkdownhighlighter.h \
     pegparser.h \
-    peghighlighterresult.h
+    peghighlighterresult.h \
+    vtexteditcompleter.h
 
 RESOURCES += \
     vnote.qrc \

+ 17 - 0
src/veditarea.h

@@ -10,10 +10,13 @@
 #include <QPair>
 #include <QSplitter>
 #include <QStack>
+#include <QSharedPointer>
+
 #include "vnotebook.h"
 #include "veditwindow.h"
 #include "vnavigationmode.h"
 #include "vfilesessioninfo.h"
+#include "vtexteditcompleter.h"
 
 class VFile;
 class VDirectory;
@@ -95,6 +98,8 @@ public:
     // Distribute all the splits evenly.
     void distributeSplits();
 
+    QSharedPointer<VTextEditCompleter> getCompleter() const;
+
 signals:
     // Emit when current window's tab status updated.
     void tabStatusUpdated(const VEditTabInfo &p_info);
@@ -244,6 +249,8 @@ private:
     bool m_autoSave;
 
     VMathJaxPreviewHelper *m_mathPreviewHelper;
+
+    QSharedPointer<VTextEditCompleter> m_completer;
 };
 
 inline VEditWindow* VEditArea::getWindow(int windowIndex) const
@@ -266,4 +273,14 @@ inline VMathJaxPreviewHelper *VEditArea::getMathJaxPreviewHelper() const
 {
     return m_mathPreviewHelper;
 }
+
+inline QSharedPointer<VTextEditCompleter> VEditArea::getCompleter() const
+{
+    if (m_completer.isNull()) {
+        VEditArea *ea = const_cast<VEditArea *>(this);
+        ea->m_completer.reset(new VTextEditCompleter(ea));
+    }
+
+    return m_completer;
+}
 #endif // VEDITAREA_H

+ 117 - 11
src/veditor.cpp

@@ -15,7 +15,9 @@ extern VConfigManager *g_config;
 
 extern VMetaWordManager *g_mwMgr;
 
-VEditor::VEditor(VFile *p_file, QWidget *p_editor)
+VEditor::VEditor(VFile *p_file,
+                 QWidget *p_editor,
+                 const QSharedPointer<VTextEditCompleter> &p_completer)
     : m_editor(p_editor),
       m_object(new VEditorObject(this, p_editor)),
       m_file(p_file),
@@ -23,12 +25,16 @@ VEditor::VEditor(VFile *p_file, QWidget *p_editor)
       m_document(nullptr),
       m_enableInputMethod(true),
       m_timeStamp(0),
-      m_trailingSpaceSelectionTS(0)
+      m_trailingSpaceSelectionTS(0),
+      m_completer(p_completer)
 {
 }
 
 VEditor::~VEditor()
 {
+    if (m_completer->widget() == m_editor) {
+        m_completer->setWidget(NULL);
+    }
 }
 
 void VEditor::init()
@@ -161,8 +167,19 @@ void VEditor::highlightOnCursorPositionChanged()
     } else {
         // Judge whether we have trailing space at current line.
         QString text = cursor.block().text();
-        if (text.rbegin()->isSpace()) {
-            updateTrailingSpaceHighlights();
+        if (!text.isEmpty()) {
+            auto it = text.rbegin();
+            bool needUpdate = it->isSpace();
+            if (!needUpdate
+                && cursor.atBlockEnd()
+                && text.size() > 1) {
+                ++it;
+                needUpdate = it->isSpace();
+            }
+
+            if (needUpdate) {
+                updateTrailingSpaceHighlights();
+            }
         }
 
         // Handle word-wrap in one block.
@@ -228,14 +245,15 @@ void VEditor::filterTrailingSpace(QList<QTextEdit::ExtraSelection> &p_selects,
                                   const QList<QTextEdit::ExtraSelection> &p_src)
 {
     QTextCursor cursor = textCursorW();
-    if (!cursor.atBlockEnd()) {
-        p_selects.append(p_src);
-        return;
-    }
-
-    int cursorPos = cursor.position();
+    bool blockEnd = cursor.atBlockEnd();
+    int blockNum = cursor.blockNumber();
     for (auto it = p_src.begin(); it != p_src.end(); ++it) {
-        if (it->cursor.selectionEnd() == cursorPos) {
+        if (blockEnd && it->cursor.blockNumber() == blockNum) {
+            // When cursor is at block end, we do not display any trailing space
+            // at current line.
+            continue;
+        } else if (!it->cursor.atBlockEnd()) {
+            // Obsolete trailing space.
             continue;
         } else {
             p_selects.append(*it);
@@ -1100,3 +1118,91 @@ bool VEditor::setCursorPosition(int p_blockNumber, int p_posInBlock)
     setTextCursorW(cursor);
     return true;
 }
+
+static Qt::CaseSensitivity completionCaseSensitivity(const QString &p_text)
+{
+    bool upperCase = false;
+    for (int i = 0; i < p_text.size(); ++i) {
+        if (p_text[i].isUpper()) {
+            upperCase = true;
+            break;
+        }
+    }
+
+    return upperCase ? Qt::CaseSensitive : Qt::CaseInsensitive;
+}
+
+void VEditor::requestCompletion(bool p_reversed)
+{
+    QTextCursor cursor = textCursorW();
+    cursor.clearSelection();
+    setTextCursorW(cursor);
+
+    QStringList words = generateCompletionCandidates();
+    QString prefix = fetchCompletionPrefix();
+    // Smart case.
+    Qt::CaseSensitivity cs = completionCaseSensitivity(prefix);
+
+    cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, prefix.size());
+    QRect popupRect = cursorRectW(cursor);
+    popupRect.setLeft(popupRect.left() + lineNumberAreaWidth());
+
+    m_completer->performCompletion(words, prefix, cs, p_reversed, popupRect, this);
+}
+
+bool VEditor::isCompletionActivated() const
+{
+    if (m_completer->widget() == m_editor
+        && m_completer->isPopupVisible()) {
+        return true;
+    }
+
+    return false;
+}
+
+QStringList VEditor::generateCompletionCandidates() const
+{
+    QString content = getContent();
+    QTextCursor cursor = textCursorW();
+    int start, end;
+    VEditUtils::findCurrentWord(cursor, start, end);
+
+    QRegExp reg("\\W+");
+
+    QStringList ret = content.mid(end).split(reg, QString::SkipEmptyParts);
+    ret.append(content.left(start).split(reg, QString::SkipEmptyParts));
+    ret.removeDuplicates();
+    return ret;
+}
+
+QString VEditor::fetchCompletionPrefix() const
+{
+    QTextCursor cursor = textCursorW();
+    if (cursor.atBlockStart()) {
+        return QString();
+    }
+
+    int pos = cursor.position() - 1;
+    int blockPos = cursor.block().position();
+    QString prefix;
+    while (pos >= blockPos) {
+        QChar ch = m_document->characterAt(pos);
+        if (ch.isSpace()) {
+            break;
+        }
+
+        prefix.prepend(ch);
+        --pos;
+    }
+
+    return prefix;
+}
+
+// @p_prefix may be longer than @p_completion.
+void VEditor::insertCompletion(const QString &p_prefix, const QString &p_completion)
+{
+    QTextCursor cursor = textCursorW();
+    cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, p_prefix.size());
+    cursor.insertText(p_completion);
+    setTextCursorW(cursor);
+}

+ 24 - 1
src/veditor.h

@@ -6,11 +6,13 @@
 #include <QList>
 #include <QTextEdit>
 #include <QColor>
+#include <QSharedPointer>
 
 #include "veditconfig.h"
 #include "vconstants.h"
 #include "vfile.h"
 #include "vwordcountinfo.h"
+#include "vtexteditcompleter.h"
 
 class QWidget;
 class VEditorObject;
@@ -39,7 +41,9 @@ enum class SelectionId {
 class VEditor
 {
 public:
-    explicit VEditor(VFile *p_file, QWidget *p_editor);
+    explicit VEditor(VFile *p_file,
+                     QWidget *p_editor,
+                     const QSharedPointer<VTextEditCompleter> &p_completer);
 
     virtual ~VEditor();
 
@@ -154,6 +158,15 @@ public:
 
     virtual bool setCursorPosition(int p_blockNumber, int p_posInBlock);
 
+    QString fetchCompletionPrefix() const;
+
+    // Request text completion.
+    virtual void requestCompletion(bool p_reversed);
+
+    virtual bool isCompletionActivated() const;
+
+    virtual void insertCompletion(const QString &p_prefix, const QString &p_completion);
+
 // Wrapper functions for QPlainTextEdit/QTextEdit.
 // Ends with W to distinguish it from the original interfaces.
 public:
@@ -201,6 +214,10 @@ public:
 
     virtual void ensureCursorVisibleW() = 0;
 
+    virtual QRect cursorRectW() = 0;
+
+    virtual QRect cursorRectW(const QTextCursor &p_cursor) = 0;
+
 protected:
     void init();
 
@@ -235,6 +252,8 @@ protected:
 
     bool handleWheelEvent(QWheelEvent *p_event);
 
+    virtual int lineNumberAreaWidth() const = 0;
+
     QWidget *m_editor;
 
     VEditorObject *m_object;
@@ -288,6 +307,8 @@ private:
     // Highlight @p_cursor as the searched keyword under cursor.
     void highlightSearchedWordUnderCursor(const QTextCursor &p_cursor);
 
+    QStringList generateCompletionCandidates() const;
+
     QLabel *m_wrapLabel;
     QTimer *m_labelTimer;
 
@@ -329,6 +350,8 @@ private:
 
     TimeStamp m_trailingSpaceSelectionTS;
 
+    QSharedPointer<VTextEditCompleter> m_completer;
+
 // Functions for private slots.
 private:
     void labelTimerTimeout();

+ 5 - 0
src/vmainwindow.cpp

@@ -3343,3 +3343,8 @@ void VMainWindow::focusEditArea() const
 
     widget->setFocus();
 }
+
+void VMainWindow::setCaptainModeEnabled(bool p_enabled)
+{
+    m_captain->setCaptainModeEnabled(p_enabled);
+}

+ 2 - 0
src/vmainwindow.h

@@ -128,6 +128,8 @@ public:
 
     VExplorer *getExplorer() const;
 
+    void setCaptainModeEnabled(bool p_enabled);
+
 signals:
     // Emit when editor related configurations were changed by user.
     void editorConfigUpdated();

+ 24 - 0
src/vmdeditoperations.cpp

@@ -431,6 +431,30 @@ bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event)
         break;
     }
 
+    case Qt::Key_N:
+    {
+        if (VUtils::isControlModifierForVim(modifiers)) {
+            // Completion.
+            if (!m_editor->isCompletionActivated()) {
+                m_editor->requestCompletion(false);
+            }
+        }
+
+        break;
+    }
+
+    case Qt::Key_P:
+    {
+        if (VUtils::isControlModifierForVim(modifiers)) {
+            // Completion.
+            if (!m_editor->isCompletionActivated()) {
+                m_editor->requestCompletion(true);
+            }
+        }
+
+        break;
+    }
+
     default:
         break;
     }

+ 7 - 1
src/vmdeditor.cpp

@@ -32,9 +32,10 @@ extern VConfigManager *g_config;
 VMdEditor::VMdEditor(VFile *p_file,
                      VDocument *p_doc,
                      MarkdownConverterType p_type,
+                     const QSharedPointer<VTextEditCompleter> &p_completer,
                      QWidget *p_parent)
     : VTextEdit(p_parent),
-      VEditor(p_file, this),
+      VEditor(p_file, this, p_completer),
       m_pegHighlighter(NULL),
       m_freshEdit(true),
       m_textToHtmlDialog(NULL),
@@ -1411,3 +1412,8 @@ void VMdEditor::setFontAndPaletteByStyleSheet(const QFont &p_font, const QPalett
                          .arg(p_palette.color(QPalette::Text).name())
                          .arg(p_palette.color(QPalette::Base).name()));
 }
+
+int VMdEditor::lineNumberAreaWidth() const
+{
+    return VTextEdit::lineNumberAreaWidth();
+}

+ 13 - 0
src/vmdeditor.h

@@ -28,6 +28,7 @@ public:
     VMdEditor(VFile *p_file,
               VDocument *p_doc,
               MarkdownConverterType p_type,
+              const QSharedPointer<VTextEditCompleter> &p_completer,
               QWidget *p_parent = nullptr);
 
     void beginEdit() Q_DECL_OVERRIDE;
@@ -189,6 +190,16 @@ public:
         ensureCursorVisible();
     }
 
+    QRect cursorRectW() Q_DECL_OVERRIDE
+    {
+        return cursorRect();
+    }
+
+    QRect cursorRectW(const QTextCursor &p_cursor) Q_DECL_OVERRIDE
+    {
+        return cursorRect(p_cursor);
+    }
+
 signals:
     // Signal when headers change.
     void headersChanged(const QVector<VTableOfContentItem> &p_headers);
@@ -223,6 +234,8 @@ protected:
 
     void wheelEvent(QWheelEvent *p_event) Q_DECL_OVERRIDE;
 
+    int lineNumberAreaWidth() const Q_DECL_OVERRIDE;
+
 private slots:
     // Update m_headers according to elements.
     void updateHeaders(const QVector<VElementRegion> &p_headerRegions);

+ 5 - 1
src/vmdtab.cpp

@@ -473,7 +473,11 @@ void VMdTab::setupMarkdownEditor()
 {
     Q_ASSERT(!m_editor);
 
-    m_editor = new VMdEditor(m_file, m_document, m_mdConType, this);
+    m_editor = new VMdEditor(m_file,
+                             m_document,
+                             m_mdConType,
+                             m_editArea->getCompleter(),
+                             this);
     m_editor->setProperty("MainEditor", true);
     m_editor->setEditTab(this);
     int delta = g_config->getEditorZoomDelta();

+ 5 - 0
src/vtextedit.cpp

@@ -459,3 +459,8 @@ void VTextEdit::dragMoveEvent(QDragMoveEvent *p_event)
     // TODO: find out the rect of current cursor to update that rect only.
     update();
 }
+
+int VTextEdit::lineNumberAreaWidth() const
+{
+    return m_lineNumberArea->width();
+}

+ 2 - 0
src/vtextedit.h

@@ -81,6 +81,8 @@ protected:
     // Return the Y offset of the content via the scrollbar.
     int contentOffsetY() const;
 
+    int lineNumberAreaWidth() const;
+
     void updateLineNumberAreaWidth(const QFontMetrics &p_metrics);
 
     void dragMoveEvent(QDragMoveEvent *p_event) Q_DECL_OVERRIDE;

+ 321 - 0
src/vtexteditcompleter.cpp

@@ -0,0 +1,321 @@
+#include "vtexteditcompleter.h"
+
+#include <QStringListModel>
+#include <QStyledItemDelegate>
+#include <QScrollBar>
+#include <QDebug>
+#include <QEvent>
+#include <QKeyEvent>
+
+#include "utils/vutils.h"
+#include "veditor.h"
+#include "vmainwindow.h"
+
+extern VMainWindow *g_mainWin;
+
+VTextEditCompleter::VTextEditCompleter(QObject *p_parent)
+    : QCompleter(p_parent),
+      m_initialized(false)
+{
+}
+
+void VTextEditCompleter::performCompletion(const QStringList &p_words,
+                                           const QString &p_prefix,
+                                           Qt::CaseSensitivity p_cs,
+                                           bool p_reversed,
+                                           const QRect &p_rect,
+                                           VEditor *p_editor)
+{
+    init();
+
+    m_editor = p_editor;
+
+    setWidget(m_editor->getEditor());
+
+    m_model->setStringList(p_words);
+    setCaseSensitivity(p_cs);
+    setCompletionPrefix(p_prefix);
+
+    int cnt = completionCount();
+    if (cnt == 0) {
+        finishCompletion();
+        return;
+    }
+
+    selectRow(p_reversed ? cnt - 1 : 0);
+
+    if (cnt == 1 && currentCompletion() == p_prefix) {
+        finishCompletion();
+        return;
+    }
+
+    g_mainWin->setCaptainModeEnabled(false);
+
+    m_insertedCompletion = p_prefix;
+    insertCurrentCompletion();
+
+    auto pu = popup();
+    QRect rt(p_rect);
+    rt.setWidth(pu->sizeHintForColumn(0) + pu->verticalScrollBar()->sizeHint().width());
+    complete(rt);
+}
+
+void VTextEditCompleter::init()
+{
+    if (m_initialized) {
+        return;
+    }
+
+    m_initialized = true;
+
+    m_model = new QStringListModel(this);
+    setModel(m_model);
+
+    popup()->setProperty("TextEdit", true);
+    popup()->setItemDelegate(new QStyledItemDelegate(this));
+
+    connect(this, static_cast<void(QCompleter::*)(const QString &)>(&QCompleter::activated),
+            this, [this](const QString &p_text) {
+                insertCompletion(p_text);
+                finishCompletion();
+            });
+}
+
+void VTextEditCompleter::selectNextCompletion(bool p_reversed)
+{
+    QModelIndex curIndex = popup()->currentIndex();
+    if (p_reversed) {
+        if (curIndex.isValid()) {
+            int row = curIndex.row();
+            if (row == 0) {
+                setCurrentIndex(QModelIndex());
+            } else {
+                selectRow(row - 1);
+            }
+        } else {
+            selectRow(completionCount() - 1);
+        }
+    } else {
+        if (curIndex.isValid()) {
+            int row = curIndex.row();
+            if (!selectRow(row + 1)) {
+                setCurrentIndex(QModelIndex());
+            }
+        } else {
+            selectRow(0);
+        }
+    }
+}
+
+bool VTextEditCompleter::selectRow(int p_row)
+{
+    if (setCurrentRow(p_row)) {
+        setCurrentIndex(currentIndex());
+        return true;
+    }
+
+    return false;
+}
+
+void VTextEditCompleter::setCurrentIndex(QModelIndex p_index, bool p_select)
+{
+    auto pu = popup();
+    if (!pu) {
+        return;
+    }
+
+    if (!p_select) {
+        pu->selectionModel()->setCurrentIndex(p_index, QItemSelectionModel::NoUpdate);
+    } else {
+        if (!p_index.isValid()) {
+            pu->selectionModel()->clear();
+        } else {
+            pu->selectionModel()->setCurrentIndex(p_index, QItemSelectionModel::ClearAndSelect);
+        }
+    }
+
+    p_index = pu->selectionModel()->currentIndex();
+    if (!p_index.isValid()) {
+        pu->scrollToTop();
+    } else {
+        pu->scrollTo(p_index);
+    }
+}
+
+bool VTextEditCompleter::eventFilter(QObject *p_obj, QEvent *p_eve)
+{
+    switch (p_eve->type()) {
+    case QEvent::KeyPress:
+    {
+        if (p_obj != popup() || !m_editor) {
+            break;
+        }
+
+        bool exited = false;
+
+        QKeyEvent *ke = static_cast<QKeyEvent *>(p_eve);
+        const int key = ke->key();
+        const int modifiers = ke->modifiers();
+        switch (key) {
+        case Qt::Key_N:
+            V_FALLTHROUGH;
+        case Qt::Key_P:
+            if (VUtils::isControlModifierForVim(modifiers)) {
+                selectNextCompletion(key == Qt::Key_P);
+                insertCurrentCompletion();
+                return true;
+            }
+            break;
+
+        case Qt::Key_J:
+            V_FALLTHROUGH;
+        case Qt::Key_K:
+            if (VUtils::isControlModifierForVim(modifiers)) {
+                selectNextCompletion(key == Qt::Key_K);
+                return true;
+            }
+            break;
+
+        case Qt::Key_BracketLeft:
+            if (!VUtils::isControlModifierForVim(modifiers)) {
+                break;
+            }
+            V_FALLTHROUGH;
+        case Qt::Key_Escape:
+            exited = true;
+            // Propogate this event to the editor widget for Vim mode.
+            if (!m_editor->getVim()) {
+                finishCompletion();
+                return true;
+            }
+            break;
+
+        case Qt::Key_E:
+            if (VUtils::isControlModifierForVim(modifiers)) {
+                cancelCompletion();
+                return true;
+            }
+            break;
+
+        case Qt::Key_Enter:
+        case Qt::Key_Return:
+        {
+            if (m_insertedCompletion != currentCompletion()) {
+                insertCurrentCompletion();
+                finishCompletion();
+                return true;
+            } else {
+                exited = true;
+            }
+            break;
+        }
+
+        default:
+            break;
+        }
+
+        int cursorPos = -1;
+        if (!exited) {
+            cursorPos = m_editor->textCursorW().position();
+        }
+
+        bool ret = QCompleter::eventFilter(p_obj, p_eve);
+        if (!exited) {
+            // Detect if cursor position changed after key press.
+            int pos = m_editor->textCursorW().position();
+            if (pos == cursorPos - 1) {
+                // Deleted one char.
+                if (m_insertedCompletion.size() > 1) {
+                    updatePrefix(m_editor->fetchCompletionPrefix());
+                } else {
+                    exited = true;
+                }
+            } else if (pos == cursorPos + 1) {
+                // Added one char.
+                QString prefix = m_editor->fetchCompletionPrefix();
+                if (prefix.size() == m_insertedCompletion.size() + 1
+                    && prefix.startsWith(m_insertedCompletion)) {
+                    updatePrefix(prefix);
+                } else {
+                    exited = true;
+                }
+            } else if (pos != cursorPos) {
+                exited = true;
+            }
+        }
+
+        if (exited) {
+            // finishCompletion() will do clean up. Must be called after QCompleter::eventFilter().
+            finishCompletion();
+        }
+
+        return ret;
+    }
+
+    case QEvent::Hide:
+    {
+        // Completion exited.
+        cleanUp();
+        break;
+    }
+
+    default:
+        break;
+    }
+
+    return QCompleter::eventFilter(p_obj, p_eve);
+}
+
+void VTextEditCompleter::insertCurrentCompletion()
+{
+    QString completion;
+    QModelIndex curIndex = popup()->currentIndex();
+    if (curIndex.isValid()) {
+        completion = currentCompletion();
+    } else {
+        completion = completionPrefix();
+    }
+
+    insertCompletion(completion);
+}
+
+void VTextEditCompleter::insertCompletion(const QString &p_completion)
+{
+    if (m_insertedCompletion == p_completion) {
+        return;
+    }
+
+    m_editor->insertCompletion(m_insertedCompletion, p_completion);
+    m_insertedCompletion = p_completion;
+}
+
+void VTextEditCompleter::cancelCompletion()
+{
+    insertCompletion(completionPrefix());
+
+    finishCompletion();
+}
+
+void VTextEditCompleter::cleanUp()
+{
+    // Do not clean up m_editor and m_insertedCompletion, since activated()
+    // signal is after the HideEvent.
+    setWidget(NULL);
+    g_mainWin->setCaptainModeEnabled(true);
+}
+
+void VTextEditCompleter::updatePrefix(const QString &p_prefix)
+{
+    m_insertedCompletion = p_prefix;
+    setCompletionPrefix(p_prefix);
+
+    int cnt = completionCount();
+    if (cnt == 0) {
+        finishCompletion();
+    } else if (cnt == 1) {
+        setCurrentRow(0);
+        if (currentCompletion() == p_prefix) {
+            finishCompletion();
+        }
+    }
+}

+ 68 - 0
src/vtexteditcompleter.h

@@ -0,0 +1,68 @@
+#ifndef VTEXTEDITCOMPLETER_H
+#define VTEXTEDITCOMPLETER_H
+
+#include <QCompleter>
+#include <QAbstractItemView>
+
+class QStringListModel;
+class VEditor;
+
+class VTextEditCompleter : public QCompleter
+{
+    Q_OBJECT
+public:
+    explicit VTextEditCompleter(QObject *p_parent = nullptr);
+
+    bool isPopupVisible() const;
+
+    void performCompletion(const QStringList &p_words,
+                           const QString &p_prefix,
+                           Qt::CaseSensitivity p_cs,
+                           bool p_reversed,
+                           const QRect &p_rect,
+                           VEditor *p_editor);
+
+protected:
+    bool eventFilter(QObject *p_obj, QEvent *p_eve) Q_DECL_OVERRIDE;
+
+private:
+    void init();
+
+    bool selectRow(int p_row);
+
+    void setCurrentIndex(QModelIndex p_index, bool p_select = true);
+
+    void selectNextCompletion(bool p_reversed);
+
+    void insertCurrentCompletion();
+
+    void insertCompletion(const QString &p_completion);
+
+    void finishCompletion();
+
+    // Revert inserted completion to prefix and finish completion.
+    void cancelCompletion();
+
+    void cleanUp();
+
+    void updatePrefix(const QString &p_prefix);
+
+    bool m_initialized;
+
+    QStringListModel *m_model;
+
+    VEditor *m_editor;
+
+    QString m_insertedCompletion;
+};
+
+inline bool VTextEditCompleter::isPopupVisible() const
+{
+    return popup()->isVisible();
+}
+
+inline void VTextEditCompleter::finishCompletion()
+{
+    popup()->hide();
+}
+#endif // VTEXTEDITCOMPLETER_H