Browse Source

support snippets

Shortcuts are not supported yet.
Le Tan 8 years ago
parent
commit
6ac33d2bd0

+ 296 - 0
src/dialog/veditsnippetdialog.cpp

@@ -0,0 +1,296 @@
+#include "veditsnippetdialog.h"
+#include <QtWidgets>
+
+#include "utils/vutils.h"
+#include "vlineedit.h"
+#include "vconfigmanager.h"
+#include "utils/vmetawordmanager.h"
+
+extern VMetaWordManager *g_mwMgr;
+
+extern VConfigManager *g_config;
+
+VEditSnippetDialog::VEditSnippetDialog(const QString &p_title,
+                                       const QString &p_info,
+                                       const QVector<VSnippet> &p_snippets,
+                                       const VSnippet &p_snippet,
+                                       QWidget *p_parent)
+    : QDialog(p_parent),
+      m_snippets(p_snippets),
+      m_snippet(p_snippet)
+{
+    setupUI(p_title, p_info);
+
+    handleInputChanged();
+}
+
+void VEditSnippetDialog::setupUI(const QString &p_title, const QString &p_info)
+{
+    QLabel *infoLabel = NULL;
+    if (!p_info.isEmpty()) {
+        infoLabel = new QLabel(p_info);
+        infoLabel->setWordWrap(true);
+    }
+
+    // Name.
+    m_nameEdit = new VLineEdit(m_snippet.getName());
+    QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp),
+                                                 m_nameEdit);
+    m_nameEdit->setValidator(validator);
+
+    // Type.
+    m_typeCB = new QComboBox();
+    for (int i = 0; i < VSnippet::Type::Invalid; ++i) {
+        m_typeCB->addItem(VSnippet::typeStr(static_cast<VSnippet::Type>(i)), i);
+    }
+
+    int typeIdx = m_typeCB->findData((int)m_snippet.getType());
+    Q_ASSERT(typeIdx > -1);
+    m_typeCB->setCurrentIndex(typeIdx);
+
+    // Shortcut.
+    m_shortcutCB = new QComboBox();
+    m_shortcutCB->addItem(tr("None"), QChar());
+    auto shortcuts = getAvailableShortcuts();
+    for (auto it : shortcuts) {
+        m_shortcutCB->addItem(it, it);
+    }
+
+    QChar sh = m_snippet.getShortcut();
+    if (sh.isNull()) {
+        m_shortcutCB->setCurrentIndex(0);
+    } else {
+        int shortcutIdx = m_shortcutCB->findData(sh);
+        m_shortcutCB->setCurrentIndex(shortcutIdx < 0 ? 0 : shortcutIdx);
+    }
+
+    // Cursor mark.
+    m_cursorMarkEdit = new QLineEdit(m_snippet.getCursorMark());
+    m_cursorMarkEdit->setToolTip(tr("String in the content to mark the cursor position"));
+
+    // Selection mark.
+    m_selectionMarkEdit = new QLineEdit(m_snippet.getSelectionMark());
+    m_selectionMarkEdit->setToolTip(tr("String in the content to be replaced with selected text"));
+
+    // Content.
+    m_contentEdit = new QTextEdit();
+    setContentEditByType();
+
+    QFormLayout *topLayout = new QFormLayout();
+    topLayout->addRow(tr("Snippet &name:"), m_nameEdit);
+    topLayout->addRow(tr("Snippet &type:"), m_typeCB);
+    topLayout->addRow(tr("Shortc&ut:"), m_shortcutCB);
+    topLayout->addRow(tr("Cursor &mark:"), m_cursorMarkEdit);
+    topLayout->addRow(tr("&Selection mark:"), m_selectionMarkEdit);
+    topLayout->addRow(tr("&Content:"), m_contentEdit);
+
+    m_warnLabel = new QLabel();
+    m_warnLabel->setWordWrap(true);
+    m_warnLabel->hide();
+
+    // Ok is the default button.
+    m_btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    connect(m_btnBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+    connect(m_btnBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+
+    QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok);
+    m_nameEdit->setMinimumWidth(okBtn->sizeHint().width() * 3);
+
+    QVBoxLayout *mainLayout = new QVBoxLayout();
+    if (infoLabel) {
+        mainLayout->addWidget(infoLabel);
+    }
+
+    mainLayout->addLayout(topLayout);
+    mainLayout->addWidget(m_warnLabel);
+    mainLayout->addWidget(m_btnBox);
+    mainLayout->setSizeConstraint(QLayout::SetFixedSize);
+    setLayout(mainLayout);
+
+    setWindowTitle(p_title);
+
+    connect(m_nameEdit, &QLineEdit::textChanged,
+            this, &VEditSnippetDialog::handleInputChanged);
+
+    connect(m_typeCB, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
+            this, &VEditSnippetDialog::handleInputChanged);
+
+    connect(m_cursorMarkEdit, &QLineEdit::textChanged,
+            this, &VEditSnippetDialog::handleInputChanged);
+
+    connect(m_selectionMarkEdit, &QLineEdit::textChanged,
+            this, &VEditSnippetDialog::handleInputChanged);
+
+    connect(m_contentEdit, &QTextEdit::textChanged,
+            this, &VEditSnippetDialog::handleInputChanged);
+}
+
+void VEditSnippetDialog::handleInputChanged()
+{
+    bool showWarnLabel = false;
+    QString name = m_nameEdit->getEvaluatedText();
+    bool nameOk = !name.isEmpty();
+    if (nameOk && name != m_snippet.getName()) {
+        // Check if the name conflicts with existing snippet name.
+        // Case-insensitive.
+        QString lowerName = name.toLower();
+        bool conflicted = false;
+        for (auto const & item : m_snippets) {
+            if (item.getName() == name) {
+                conflicted = true;
+                break;
+            }
+        }
+
+        QString warnText;
+        if (conflicted) {
+            nameOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Name (case-insensitive) <span style=\"%2\">%3</span> already exists. "
+                          "Please choose another name.")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(name);
+        } else if (!VUtils::checkFileNameLegal(name)) {
+            // Check if evaluated name contains illegal characters.
+            nameOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Name <span style=\"%2\">%3</span> contains illegal characters "
+                          "(after magic word evaluation).")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(name);
+        }
+
+        if (!nameOk) {
+            showWarnLabel = true;
+            m_warnLabel->setText(warnText);
+        }
+    }
+
+    QString cursorMark = m_cursorMarkEdit->text();
+    bool cursorMarkOk = true;
+    if (nameOk && !cursorMark.isEmpty()) {
+        // Check if the mark appears more than once in the content.
+        QString selectionMark = m_selectionMarkEdit->text();
+        QString content = getContentEditByType();
+        content = g_mwMgr->evaluate(content);
+        QString warnText;
+        if (content.count(cursorMark) > 1) {
+            cursorMarkOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Cursor mark <span style=\"%2\">%3</span> occurs more than once "
+                          "in the content (after magic word evaluation). "
+                          "Please choose another mark.")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(cursorMark);
+        } else if ((cursorMark == selectionMark
+                    || cursorMark.contains(selectionMark)
+                    || selectionMark.contains(cursorMark))
+                   && !selectionMark.isEmpty()) {
+            cursorMarkOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Cursor mark <span style=\"%2\">%3</span> conflicts with selection mark. "
+                          "Please choose another mark.")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(cursorMark);
+        }
+
+        if (!cursorMarkOk) {
+            showWarnLabel = true;
+            m_warnLabel->setText(warnText);
+        }
+    }
+
+    m_warnLabel->setVisible(showWarnLabel);
+
+    QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok);
+    okBtn->setEnabled(nameOk && cursorMarkOk);
+}
+
+void VEditSnippetDialog::setContentEditByType()
+{
+    switch (m_snippet.getType()) {
+    case VSnippet::Type::PlainText:
+        m_contentEdit->setPlainText(m_snippet.getContent());
+        break;
+
+    case VSnippet::Type::Html:
+        m_contentEdit->setHtml(m_snippet.getContent());
+        break;
+
+    default:
+        m_contentEdit->setPlainText(m_snippet.getContent());
+        break;
+    }
+}
+
+QString VEditSnippetDialog::getContentEditByType() const
+{
+    if (m_typeCB->currentIndex() == -1) {
+        return QString();
+    }
+
+    switch (static_cast<VSnippet::Type>(m_typeCB->currentData().toInt())) {
+    case VSnippet::Type::PlainText:
+        return m_contentEdit->toPlainText();
+
+    case VSnippet::Type::Html:
+        return m_contentEdit->toHtml();
+
+    default:
+        return m_contentEdit->toPlainText();
+    }
+}
+
+QString VEditSnippetDialog::getNameInput() const
+{
+    return m_nameEdit->getEvaluatedText();
+}
+
+VSnippet::Type VEditSnippetDialog::getTypeInput() const
+{
+    return static_cast<VSnippet::Type>(m_typeCB->currentData().toInt());
+}
+
+QString VEditSnippetDialog::getCursorMarkInput() const
+{
+    return m_cursorMarkEdit->text();
+}
+
+QString VEditSnippetDialog::getSelectionMarkInput() const
+{
+    return m_selectionMarkEdit->text();
+}
+
+QString VEditSnippetDialog::getContentInput() const
+{
+    return getContentEditByType();
+}
+
+QChar VEditSnippetDialog::getShortcutInput() const
+{
+    return m_shortcutCB->currentData().toChar();
+}
+
+QVector<QChar> VEditSnippetDialog::getAvailableShortcuts() const
+{
+    QVector<QChar> ret = VSnippet::getAllShortcuts();
+
+    QChar curCh = m_snippet.getShortcut();
+    // Remove those have already been assigned to snippets.
+    for (auto const & snip : m_snippets) {
+        QChar ch = snip.getShortcut();
+        if (ch.isNull()) {
+            continue;
+        }
+
+        if (ch != curCh) {
+            ret.removeOne(ch);
+        }
+    }
+
+    return ret;
+}

+ 66 - 0
src/dialog/veditsnippetdialog.h

@@ -0,0 +1,66 @@
+#ifndef VEDITSNIPPETDIALOG_H
+#define VEDITSNIPPETDIALOG_H
+
+#include <QDialog>
+#include <QVector>
+
+#include "vsnippet.h"
+
+class VLineEdit;
+class QLineEdit;
+class QLabel;
+class QDialogButtonBox;
+class QComboBox;
+class QTextEdit;
+
+
+class VEditSnippetDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    VEditSnippetDialog(const QString &p_title,
+                       const QString &p_info,
+                       const QVector<VSnippet> &p_snippets,
+                       const VSnippet &p_snippet,
+                       QWidget *p_parent = nullptr);
+
+    QString getNameInput() const;
+
+    VSnippet::Type getTypeInput() const;
+
+    QString getCursorMarkInput() const;
+
+    QString getSelectionMarkInput() const;
+
+    QString getContentInput() const;
+
+    QChar getShortcutInput() const;
+
+private slots:
+    void handleInputChanged();
+
+private:
+    void setupUI(const QString &p_title, const QString &p_info);
+
+    void setContentEditByType();
+
+    QString getContentEditByType() const;
+
+    QVector<QChar> getAvailableShortcuts() const;
+
+    VLineEdit *m_nameEdit;
+    QComboBox *m_typeCB;
+    QComboBox *m_shortcutCB;
+    QLineEdit *m_cursorMarkEdit;
+    QLineEdit *m_selectionMarkEdit;
+    QTextEdit *m_contentEdit;
+
+    QLabel *m_warnLabel;
+    QDialogButtonBox *m_btnBox;
+
+    const QVector<VSnippet> &m_snippets;
+
+    const VSnippet &m_snippet;
+};
+
+#endif // VEDITSNIPPETDIALOG_H

+ 7 - 0
src/resources/icons/add_snippet.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<polygon points="448,224 288,224 288,64 224,64 224,224 64,224 64,288 224,288 224,448 288,448 288,288 448,288 "/>
+</svg>

+ 16 - 0
src/resources/icons/apply_snippet.svg

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<g>
+	<polygon points="198.011,159.22 163.968,193.337 420.064,450 454,415.883 	"/>
+	<rect x="182" y="62" width="32" height="64"/>
+	<rect x="182" y="266" width="32" height="64"/>
+	<rect x="274" y="178" width="64" height="32"/>
+	<polygon points="303.941,112.143 281.314,89.465 236.06,134.82 258.687,157.498 	"/>
+	<polygon points="92.06,112.143 137.314,157.498 159.941,134.82 114.687,89.465 	"/>
+	<polygon points="92.06,279.141 114.687,301.816 159.941,256.462 137.314,233.784 	"/>
+	<rect x="58" y="178" width="64" height="32"/>
+</g>
+</svg>

+ 10 - 0
src/resources/icons/delete_snippet.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<path d="M341,128V99c0-19.1-14.5-35-34.5-35H205.4C185.5,64,171,79.9,171,99v29H80v32h9.2c0,0,5.4,0.6,8.2,3.4c2.8,2.8,3.9,9,3.9,9
+	l19,241.7c1.5,29.4,1.5,33.9,36,33.9h199.4c34.5,0,34.5-4.4,36-33.8l19-241.6c0,0,1.1-6.3,3.9-9.1c2.8-2.8,8.2-3.4,8.2-3.4h9.2v-32
+	h-91V128z M192,99c0-9.6,7.8-15,17.7-15h91.7c9.9,0,18.6,5.5,18.6,15v29H192V99z M183.5,384l-10.3-192h20.3L204,384H183.5z
+	 M267.1,384h-22V192h22V384z M328.7,384h-20.4l10.5-192h20.3L328.7,384z"/>
+</svg>

+ 10 - 0
src/resources/icons/locate_snippet.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<path d="M437.334,144H256.006l-42.668-48H74.666C51.197,96,32,115.198,32,138.667v234.666C32,396.802,51.197,416,74.666,416h362.668
+	C460.803,416,480,396.802,480,373.333V186.667C480,163.198,460.803,144,437.334,144z M448,373.333
+	c0,5.782-4.885,10.667-10.666,10.667H74.666C68.884,384,64,379.115,64,373.333V176h373.334c5.781,0,10.666,4.885,10.666,10.667
+	V373.333z"/>
+</svg>

+ 10 - 0
src/resources/icons/snippet_info.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<g>
+	<polygon points="288,448 288,192 192,192 192,208 224,208 224,448 192,448 192,464 320,464 320,448 	"/>
+	<path d="M255.8,144.5c26.6,0,48.2-21.6,48.2-48.2s-21.6-48.2-48.2-48.2c-26.6,0-48.2,21.6-48.2,48.2S229.2,144.5,255.8,144.5z"/>
+</g>
+</svg>

+ 16 - 0
src/resources/icons/snippets.svg

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<g>
+	<polygon points="198.011,159.22 163.968,193.337 420.064,450 454,415.883 	"/>
+	<rect x="182" y="62" width="32" height="64"/>
+	<rect x="182" y="266" width="32" height="64"/>
+	<rect x="274" y="178" width="64" height="32"/>
+	<polygon points="303.941,112.143 281.314,89.465 236.06,134.82 258.687,157.498 	"/>
+	<polygon points="92.06,112.143 137.314,157.498 159.941,134.82 114.687,89.465 	"/>
+	<polygon points="92.06,279.141 114.687,301.816 159.941,256.462 137.314,233.784 	"/>
+	<rect x="58" y="178" width="64" height="32"/>
+</g>
+</svg>

+ 11 - 2
src/src.pro

@@ -57,6 +57,7 @@ SOURCES += main.cpp\
     dialog/vselectdialog.cpp \
     vcaptain.cpp \
     vopenedlistmenu.cpp \
+    vnavigationmode.cpp \
     vorphanfile.cpp \
     vcodeblockhighlighthelper.cpp \
     vwebview.cpp \
@@ -91,7 +92,11 @@ SOURCES += main.cpp\
     vpreviewmanager.cpp \
     vimageresourcemanager2.cpp \
     vtextdocumentlayout.cpp \
-    vtextedit.cpp
+    vtextedit.cpp \
+    vsnippetlist.cpp \
+    vsnippet.cpp \
+    dialog/veditsnippetdialog.cpp \
+    utils/vimnavigationforwidget.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -170,7 +175,11 @@ HEADERS  += vmainwindow.h \
     vpreviewmanager.h \
     vimageresourcemanager2.h \
     vtextdocumentlayout.h \
-    vtextedit.h
+    vtextedit.h \
+    vsnippetlist.h \
+    vsnippet.h \
+    dialog/veditsnippetdialog.h \
+    utils/vimnavigationforwidget.h
 
 RESOURCES += \
     vnote.qrc \

+ 69 - 0
src/utils/vimnavigationforwidget.cpp

@@ -0,0 +1,69 @@
+#include "vimnavigationforwidget.h"
+
+#include <QWidget>
+#include <QCoreApplication>
+#include <QKeyEvent>
+#include <QDebug>
+
+#include "vutils.h"
+
+VimNavigationForWidget::VimNavigationForWidget()
+{
+}
+
+bool VimNavigationForWidget::injectKeyPressEventForVim(QWidget *p_widget,
+                                                       QKeyEvent *p_event,
+                                                       QWidget *p_escWidget)
+{
+    Q_ASSERT(p_widget);
+
+    bool ret = false;
+    int key = p_event->key();
+    int modifiers = p_event->modifiers();
+    if (!p_escWidget) {
+        p_escWidget = p_widget;
+    }
+
+    switch (key) {
+    case Qt::Key_BracketLeft:
+    {
+        if (VUtils::isControlModifierForVim(modifiers)) {
+            QKeyEvent *escEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Escape,
+                                                Qt::NoModifier);
+            QCoreApplication::postEvent(p_escWidget, escEvent);
+            ret = true;
+        }
+
+        break;
+    }
+
+    case Qt::Key_J:
+    {
+        if (VUtils::isControlModifierForVim(modifiers)) {
+            QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down,
+                                                 Qt::NoModifier);
+            QCoreApplication::postEvent(p_widget, downEvent);
+            ret = true;
+        }
+
+        break;
+    }
+
+    case Qt::Key_K:
+    {
+        if (VUtils::isControlModifierForVim(modifiers)) {
+            QKeyEvent *upEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up,
+                                               Qt::NoModifier);
+            QCoreApplication::postEvent(p_widget, upEvent);
+            ret = true;
+        }
+
+        break;
+    }
+
+    default:
+        break;
+    }
+
+    return ret;
+}

+ 24 - 0
src/utils/vimnavigationforwidget.h

@@ -0,0 +1,24 @@
+#ifndef VIMNAVIGATIONFORWIDGET_H
+#define VIMNAVIGATIONFORWIDGET_H
+
+class QWidget;
+class QKeyEvent;
+
+
+// Provide simple Vim mode navigation for widgets.
+class VimNavigationForWidget
+{
+public:
+    // Try to handle @p_event and inject proper event instead if it triggers
+    // Vim operation.
+    // Return true if @p_event is handled properly.
+    // @p_escWidget: the widget to accept the ESC event.
+    static bool injectKeyPressEventForVim(QWidget *p_widget,
+                                          QKeyEvent *p_event,
+                                          QWidget *p_escWidget = nullptr);
+
+private:
+    VimNavigationForWidget();
+};
+
+#endif // VIMNAVIGATIONFORWIDGET_H

+ 44 - 1
src/utils/vutils.cpp

@@ -61,7 +61,7 @@ QString VUtils::readFileFromDisk(const QString &filePath)
 {
     QFile file(filePath);
     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
-        qWarning() << "fail to read file" << filePath;
+        qWarning() << "fail to open file" << filePath << "to read";
         return QString();
     }
     QString fileText(file.readAll());
@@ -84,6 +84,34 @@ bool VUtils::writeFileToDisk(const QString &filePath, const QString &text)
     return true;
 }
 
+bool VUtils::writeJsonToDisk(const QString &p_filePath, const QJsonObject &p_json)
+{
+    QFile file(p_filePath);
+    // We use Unix LF for config file.
+    if (!file.open(QIODevice::WriteOnly)) {
+        qWarning() << "fail to open file" << p_filePath << "to write";
+        return false;
+    }
+
+    QJsonDocument doc(p_json);
+    if (-1 == file.write(doc.toJson())) {
+        return false;
+    }
+
+    return true;
+}
+
+QJsonObject VUtils::readJsonFromDisk(const QString &p_filePath)
+{
+    QFile file(p_filePath);
+    if (!file.open(QIODevice::ReadOnly)) {
+        qWarning() << "fail to open file" << p_filePath << "to read";
+        return QJsonObject();
+    }
+
+    return QJsonDocument::fromJson(file.readAll()).object();
+}
+
 QRgb VUtils::QRgbFromString(const QString &str)
 {
     Q_ASSERT(str.length() == 6);
@@ -859,6 +887,12 @@ bool VUtils::deleteFile(const VNotebook *p_notebook,
     }
 }
 
+bool VUtils::deleteFile(const QString &p_path)
+{
+    QFile file(p_path);
+    return file.remove();
+}
+
 bool VUtils::deleteFile(const VOrphanFile *p_file,
                         const QString &p_path,
                         bool p_skipRecycleBin)
@@ -1004,3 +1038,12 @@ QString VUtils::validFilePathToOpen(const QString &p_file)
 
     return QString();
 }
+
+bool VUtils::isControlModifierForVim(int p_modifiers)
+{
+#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
+    return p_modifiers == Qt::MetaModifier;
+#else
+    return p_modifiers == Qt::ControlModifier;
+#endif
+}

+ 13 - 0
src/utils/vutils.h

@@ -72,9 +72,16 @@ class VUtils
 {
 public:
     static QString readFileFromDisk(const QString &filePath);
+
     static bool writeFileToDisk(const QString &filePath, const QString &text);
+
+    static bool writeJsonToDisk(const QString &p_filePath, const QJsonObject &p_json);
+
+    static QJsonObject readJsonFromDisk(const QString &p_filePath);
+
     // Transform FFFFFF string to QRgb
     static QRgb QRgbFromString(const QString &str);
+
     static QString generateImageFileName(const QString &path, const QString &title,
                                          const QString &format = "png");
 
@@ -226,6 +233,9 @@ public:
                            const QString &p_path,
                            bool p_skipRecycleBin = false);
 
+    // Delete file specified by @p_path.
+    static bool deleteFile(const QString &p_path);
+
     static QString displayDateTime(const QDateTime &p_dateTime);
 
     // Check if file @p_name exists in @p_dir.
@@ -242,6 +252,9 @@ public:
     // Return empty if it is not valid.
     static QString validFilePathToOpen(const QString &p_file);
 
+    // See if @p_modifiers is Control which is different on macOs and Windows.
+    static bool isControlModifierForVim(int p_modifiers);
+
     // Regular expression for image link.
     // ![image title]( http://github.com/tamlok/vnote.jpg "alt \" text" )
     // Captured texts (need to be trimmed):

+ 12 - 22
src/utils/vvim.cpp

@@ -24,16 +24,6 @@ const int VVim::SearchHistory::c_capacity = 50;
 
 #define ADDKEY(x, y) case (x): {ch = (y); break;}
 
-// See if @p_modifiers is Control which is different on macOs and Windows.
-static bool isControlModifier(int p_modifiers)
-{
-#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
-    return p_modifiers == Qt::MetaModifier;
-#else
-    return p_modifiers == Qt::ControlModifier;
-#endif
-}
-
 // Returns NULL QChar if invalid.
 static QChar keyToChar(int p_key, int p_modifiers)
 {
@@ -41,7 +31,7 @@ static QChar keyToChar(int p_key, int p_modifiers)
         return QChar('0' + (p_key - Qt::Key_0));
     } else if (p_key >= Qt::Key_A && p_key <= Qt::Key_Z) {
         if (p_modifiers == Qt::ShiftModifier
-            || isControlModifier(p_modifiers)) {
+            || VUtils::isControlModifierForVim(p_modifiers)) {
             return QChar('A' + (p_key - Qt::Key_A));
         } else {
             return QChar('a' + (p_key - Qt::Key_A));
@@ -99,7 +89,7 @@ static QString keyToString(int p_key, int p_modifiers)
         return QString();
     }
 
-    if (isControlModifier(p_modifiers)) {
+    if (VUtils::isControlModifierForVim(p_modifiers)) {
         return QString("^") + ch;
     } else {
         return ch;
@@ -473,7 +463,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
     // Handle Insert mode key press.
     if (VimMode::Insert == m_mode) {
         if (key == Qt::Key_Escape
-            || (key == Qt::Key_BracketLeft && isControlModifier(modifiers))) {
+            || (key == Qt::Key_BracketLeft && VUtils::isControlModifierForVim(modifiers))) {
             // See if we need to cancel auto indent.
             bool cancelAutoIndent = false;
             if (p_autoIndentPos && *p_autoIndentPos > -1) {
@@ -510,14 +500,14 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
             }
 
             goto clear_accept;
-        } else if (key == Qt::Key_R && isControlModifier(modifiers)) {
+        } else if (key == Qt::Key_R && VUtils::isControlModifierForVim(modifiers)) {
             // Ctrl+R, insert the content of a register.
             m_pendingKeys.append(keyInfo);
             m_registerPending = true;
             goto accept;
         }
 
-        if (key == Qt::Key_O && isControlModifier(modifiers)) {
+        if (key == Qt::Key_O && VUtils::isControlModifierForVim(modifiers)) {
             // Ctrl+O, enter normal mode, execute one command, then return to insert mode.
             m_insertModeAfterCommand = true;
             clearSelection();
@@ -823,7 +813,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
 
             m_editor->setTextCursorW(cursor);
             setMode(VimMode::Insert);
-        } else if (isControlModifier(modifiers)) {
+        } else if (VUtils::isControlModifierForVim(modifiers)) {
             // Ctrl+I, jump to next location.
             if (!m_tokens.isEmpty()
                 || !checkMode(VimMode::Normal)) {
@@ -935,7 +925,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
             }
 
             break;
-        } else if (isControlModifier(modifiers)) {
+        } else if (VUtils::isControlModifierForVim(modifiers)) {
             // Ctrl+O, jump to previous location.
             if (!m_tokens.isEmpty()
                 || !checkMode(VimMode::Normal)) {
@@ -1037,7 +1027,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
     // Should be kept together with Qt::Key_PageUp.
     case Qt::Key_B:
     {
-        if (isControlModifier(modifiers)) {
+        if (VUtils::isControlModifierForVim(modifiers)) {
             // Ctrl+B, page up, fall through.
             modifiers = Qt::NoModifier;
         } else if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) {
@@ -1095,7 +1085,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
         tryGetRepeatToken(m_keys, m_tokens);
         bool toLower = modifiers == Qt::NoModifier;
 
-        if (isControlModifier(modifiers)) {
+        if (VUtils::isControlModifierForVim(modifiers)) {
             // Ctrl+U, HalfPageUp.
             if (!m_keys.isEmpty()) {
                 // Not a valid sequence.
@@ -1200,7 +1190,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
 
     case Qt::Key_D:
     {
-        if (isControlModifier(modifiers)) {
+        if (VUtils::isControlModifierForVim(modifiers)) {
             // Ctrl+D, HalfPageDown.
             tryGetRepeatToken(m_keys, m_tokens);
             if (!m_keys.isEmpty()) {
@@ -1301,7 +1291,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
     // Should be kept together with Qt::Key_Escape.
     case Qt::Key_BracketLeft:
     {
-        if (isControlModifier(modifiers)) {
+        if (VUtils::isControlModifierForVim(modifiers)) {
             // fallthrough.
         } else if (modifiers == Qt::NoModifier) {
             tryGetRepeatToken(m_keys, m_tokens);
@@ -1792,7 +1782,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
             break;
         }
 
-        if (isControlModifier(modifiers)) {
+        if (VUtils::isControlModifierForVim(modifiers)) {
             // Redo.
             tryGetRepeatToken(m_keys, m_tokens);
             if (!m_keys.isEmpty() || hasActionToken()) {

+ 5 - 41
src/vattachmentlist.cpp

@@ -8,6 +8,7 @@
 #include "vmainwindow.h"
 #include "dialog/vconfirmdeletiondialog.h"
 #include "dialog/vsortdialog.h"
+#include "utils/vimnavigationforwidget.h"
 
 extern VConfigManager *g_config;
 extern VMainWindow *g_mainWin;
@@ -462,47 +463,10 @@ void VAttachmentList::handleListItemCommitData(QWidget *p_itemEdit)
 
 void VAttachmentList::keyPressEvent(QKeyEvent *p_event)
 {
-    int key = p_event->key();
-    int modifiers = p_event->modifiers();
-    switch (key) {
-    case Qt::Key_BracketLeft:
-    {
-        if (modifiers == Qt::ControlModifier) {
-            QKeyEvent *escEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Escape,
-                                                Qt::NoModifier);
-            QCoreApplication::postEvent(this, escEvent);
-            return;
-        }
-
-        break;
-    }
-
-    case Qt::Key_J:
-    {
-        if (modifiers == Qt::ControlModifier) {
-            QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down,
-                                                 Qt::NoModifier);
-            QCoreApplication::postEvent(m_attachmentList, downEvent);
-            return;
-        }
-
-        break;
-    }
-
-    case Qt::Key_K:
-    {
-        if (modifiers == Qt::ControlModifier) {
-            QKeyEvent *upEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up,
-                                               Qt::NoModifier);
-            QCoreApplication::postEvent(m_attachmentList, upEvent);
-            return;
-        }
-
-        break;
-    }
-
-    default:
-        break;
+    if (VimNavigationForWidget::injectKeyPressEventForVim(m_attachmentList,
+                                                          p_event,
+                                                          this)) {
+        return;
     }
 
     QWidget::keyPressEvent(p_event);

+ 23 - 3
src/vconfigmanager.cpp

@@ -28,12 +28,16 @@ const QString VConfigManager::c_defaultConfigFile = QString("vnote.ini");
 
 const QString VConfigManager::c_sessionConfigFile = QString("session.ini");
 
+const QString VConfigManager::c_snippetConfigFile = QString("snippet.json");
+
 const QString VConfigManager::c_styleConfigFolder = QString("styles");
 
 const QString VConfigManager::c_codeBlockStyleConfigFolder = QString("codeblock_styles");
 
 const QString VConfigManager::c_templateConfigFolder = QString("templates");
 
+const QString VConfigManager::c_snippetConfigFolder = QString("snippets");
+
 const QString VConfigManager::c_defaultCssFile = QString(":/resources/styles/default.css");
 
 const QString VConfigManager::c_defaultCodeBlockCssFile = QString(":/utils/highlightjs/styles/vnote.css");
@@ -483,6 +487,7 @@ bool VConfigManager::writeDirectoryConfig(const QString &path, const QJsonObject
     QString configFile = fetchDirConfigFilePath(path);
 
     QFile config(configFile);
+    // We use Unix LF for config file.
     if (!config.open(QIODevice::WriteOnly)) {
         qWarning() << "fail to open directory configuration file for write:"
                    << configFile;
@@ -734,17 +739,32 @@ QString VConfigManager::getConfigFilePath() const
 
 QString VConfigManager::getStyleConfigFolder() const
 {
-    return getConfigFolder() + QDir::separator() + c_styleConfigFolder;
+    static QString path = QDir(getConfigFolder()).filePath(c_styleConfigFolder);
+    return path;
 }
 
 QString VConfigManager::getCodeBlockStyleConfigFolder() const
 {
-    return getStyleConfigFolder() + QDir::separator() + c_codeBlockStyleConfigFolder;
+    static QString path = QDir(getStyleConfigFolder()).filePath(c_codeBlockStyleConfigFolder);
+    return path;
 }
 
 QString VConfigManager::getTemplateConfigFolder() const
 {
-    return getConfigFolder() + QDir::separator() + c_templateConfigFolder;
+    static QString path = QDir(getConfigFolder()).filePath(c_templateConfigFolder);
+    return path;
+}
+
+QString VConfigManager::getSnippetConfigFolder() const
+{
+    static QString path = QDir(getConfigFolder()).filePath(c_snippetConfigFolder);
+    return path;
+}
+
+QString VConfigManager::getSnippetConfigFilePath() const
+{
+    static QString path = QDir(getSnippetConfigFolder()).filePath(c_snippetConfigFile);
+    return path;
 }
 
 QVector<QString> VConfigManager::getCssStyles() const

+ 11 - 0
src/vconfigmanager.h

@@ -352,6 +352,11 @@ public:
     // Get the folder c_templateConfigFolder in the config folder.
     QString getTemplateConfigFolder() const;
 
+    // Get the folder c_snippetConfigFolder in the config folder.
+    QString getSnippetConfigFolder() const;
+
+    QString getSnippetConfigFilePath() const;
+
     // Read all available css files in c_styleConfigFolder.
     QVector<QString> getCssStyles() const;
 
@@ -714,6 +719,9 @@ private:
     // The name of the config file for session information.
     static const QString c_sessionConfigFile;
 
+    // The name of the config file for snippets folder.
+    static const QString c_snippetConfigFile;
+
     // QSettings for the user configuration
     QSettings *userSettings;
 
@@ -733,6 +741,9 @@ private:
     // The folder name of template files.
     static const QString c_templateConfigFolder;
 
+    // The folder name of snippet files.
+    static const QString c_snippetConfigFolder;
+
     // Default CSS file in resource system.
     static const QString c_defaultCssFile;
 

+ 12 - 0
src/vconstants.h

@@ -50,6 +50,18 @@ namespace DirConfig
     static const QString c_modifiedTime = "modified_time";
 }
 
+// Snippet Cofnig file items.
+namespace SnippetConfig
+{
+    static const QString c_version = "version";
+    static const QString c_snippets = "snippets";
+    static const QString c_name = "name";
+    static const QString c_type = "type";
+    static const QString c_cursorMark = "cursor_mark";
+    static const QString c_selectionMark = "selection_mark";
+    static const QString c_shortcut = "shortcut";
+}
+
 static const QString c_emptyHeaderName = "[EMPTY]";
 
 enum class TextDecoration

+ 10 - 123
src/vdirectorytree.cpp

@@ -10,6 +10,7 @@
 #include "vconfigmanager.h"
 #include "vmainwindow.h"
 #include "dialog/vsortdialog.h"
+#include "utils/vimnavigationforwidget.h"
 
 extern VMainWindow *g_mainWin;
 
@@ -910,6 +911,10 @@ void VDirectoryTree::mousePressEvent(QMouseEvent *event)
 
 void VDirectoryTree::keyPressEvent(QKeyEvent *event)
 {
+    if (VimNavigationForWidget::injectKeyPressEventForVim(this, event)) {
+        return;
+    }
+
     int key = event->key();
     int modifiers = event->modifiers();
 
@@ -924,32 +929,6 @@ void VDirectoryTree::keyPressEvent(QKeyEvent *event)
         break;
     }
 
-    case Qt::Key_J:
-    {
-        if (modifiers == Qt::ControlModifier) {
-            event->accept();
-            QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down,
-                                                 Qt::NoModifier);
-            QCoreApplication::postEvent(this, downEvent);
-            return;
-        }
-
-        break;
-    }
-
-    case Qt::Key_K:
-    {
-        if (modifiers == Qt::ControlModifier) {
-            event->accept();
-            QKeyEvent *upEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up,
-                                               Qt::NoModifier);
-            QCoreApplication::postEvent(this, upEvent);
-            return;
-        }
-
-        break;
-    }
-
     case Qt::Key_Asterisk:
     {
         if (modifiers == Qt::ShiftModifier) {
@@ -1089,110 +1068,18 @@ void VDirectoryTree::expandSubTree(QTreeWidgetItem *p_item)
     }
 }
 
-void VDirectoryTree::registerNavigation(QChar p_majorKey)
-{
-    m_majorKey = p_majorKey;
-    V_ASSERT(m_keyMap.empty());
-    V_ASSERT(m_naviLabels.empty());
-}
-
 void VDirectoryTree::showNavigation()
 {
-    // Clean up.
-    m_keyMap.clear();
-    for (auto label : m_naviLabels) {
-        delete label;
-    }
-    m_naviLabels.clear();
-
-    if (!isVisible()) {
-        return;
-    }
-
-    // Generate labels for visible items.
-    auto items = getVisibleItems();
-    for (int i = 0; i < 26 && i < items.size(); ++i) {
-        QChar key('a' + i);
-        m_keyMap[key] = items[i];
-
-        QString str = QString(m_majorKey) + key;
-        QLabel *label = new QLabel(str, this);
-        label->setStyleSheet(g_vnote->getNavigationLabelStyle(str));
-        label->move(visualItemRect(items[i]).topLeft());
-        label->show();
-        m_naviLabels.append(label);
-    }
-}
-
-void VDirectoryTree::hideNavigation()
-{
-    m_keyMap.clear();
-    for (auto label : m_naviLabels) {
-        delete label;
-    }
-    m_naviLabels.clear();
+    VNavigationMode::showNavigation(this);
 }
 
 bool VDirectoryTree::handleKeyNavigation(int p_key, bool &p_succeed)
 {
     static bool secondKey = false;
-    bool ret = false;
-    p_succeed = false;
-    QChar keyChar = VUtils::keyToChar(p_key);
-    if (secondKey && !keyChar.isNull()) {
-        secondKey = false;
-        p_succeed = true;
-        ret = true;
-        auto it = m_keyMap.find(keyChar);
-        if (it != m_keyMap.end()) {
-            setCurrentItem(it.value());
-            setFocus();
-        }
-    } else if (keyChar == m_majorKey) {
-        // Major key pressed.
-        // Need second key if m_keyMap is not empty.
-        if (m_keyMap.isEmpty()) {
-            p_succeed = true;
-        } else {
-            secondKey = true;
-        }
-        ret = true;
-    }
-    return ret;
-}
-
-QList<QTreeWidgetItem *> VDirectoryTree::getVisibleItems() const
-{
-    QList<QTreeWidgetItem *> items;
-    for (int i = 0; i < topLevelItemCount(); ++i) {
-        QTreeWidgetItem *item = topLevelItem(i);
-        if (!item->isHidden()) {
-            items.append(item);
-            if (item->isExpanded()) {
-                items.append(getVisibleChildItems(item));
-            }
-        }
-    }
-
-    return items;
-}
-
-QList<QTreeWidgetItem *> VDirectoryTree::getVisibleChildItems(const QTreeWidgetItem *p_item) const
-{
-    QList<QTreeWidgetItem *> items;
-    if (p_item && !p_item->isHidden() && p_item->isExpanded()) {
-        for (int i = 0; i < p_item->childCount(); ++i) {
-            QTreeWidgetItem *child = p_item->child(i);
-            if (!child->isHidden()) {
-                items.append(child);
-                if (child->isExpanded()) {
-                    items.append(getVisibleChildItems(child));
-                }
-            }
-        }
-    }
-
-    return items;
+    return VNavigationMode::handleKeyNavigation(this,
+                                                secondKey,
+                                                p_key,
+                                                p_succeed);
 }
 
 int VDirectoryTree::getNewMagic()

+ 0 - 11
src/vdirectorytree.h

@@ -29,9 +29,7 @@ public:
     const VNotebook *currentNotebook() const;
 
     // Implementations for VNavigationMode.
-    void registerNavigation(QChar p_majorKey) Q_DECL_OVERRIDE;
     void showNavigation() Q_DECL_OVERRIDE;
-    void hideNavigation() Q_DECL_OVERRIDE;
     bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE;
 
 signals:
@@ -134,10 +132,6 @@ private:
     // Expand the currently-built subtree of @p_item according to VDirectory.isExpanded().
     void expandSubTree(QTreeWidgetItem *p_item);
 
-    QList<QTreeWidgetItem *> getVisibleItems() const;
-
-    QList<QTreeWidgetItem *> getVisibleChildItems(const QTreeWidgetItem *p_item) const;
-
     // We use a map to save and restore current directory of each notebook.
     // Try to restore current directory after changing notebook.
     // Return false if no cache item found for current notebook.
@@ -180,11 +174,6 @@ private:
     // Reload content from disk.
     QAction *m_reloadAct;
 
-    // Navigation Mode.
-    // Map second key to QTreeWidgetItem.
-    QMap<QChar, QTreeWidgetItem *> m_keyMap;
-    QVector<QLabel *> m_naviLabels;
-
     static const QString c_infoShortcutSequence;
     static const QString c_copyShortcutSequence;
     static const QString c_cutShortcutSequence;

+ 2 - 1
src/veditarea.cpp

@@ -756,7 +756,8 @@ bool VEditArea::handleKeyNavigation(int p_key, bool &p_succeed)
         ret = true;
         auto it = m_keyMap.find(keyChar);
         if (it != m_keyMap.end()) {
-            setCurrentWindow(splitter->indexOf(it.value()), true);
+            setCurrentWindow(splitter->indexOf(static_cast<VEditWindow *>(it.value())),
+                                               true);
         }
     } else if (keyChar == m_majorKey) {
         // Major key pressed.

+ 0 - 5
src/veditarea.h

@@ -205,11 +205,6 @@ private:
 
     // Last closed files stack.
     QStack<VFileSessionInfo> m_lastClosedFiles;
-
-    // Navigation Mode.
-    // Map second key to VEditWindow.
-    QMap<QChar, VEditWindow *> m_keyMap;
-    QVector<QLabel *> m_naviLabels;
 };
 
 inline VEditWindow* VEditArea::getWindow(int windowIndex) const

+ 8 - 0
src/veditor.cpp

@@ -9,6 +9,7 @@
 #include "veditoperations.h"
 #include "dialog/vinsertlinkdialog.h"
 #include "utils/vmetawordmanager.h"
+#include "utils/vvim.h"
 
 extern VConfigManager *g_config;
 
@@ -915,3 +916,10 @@ void VEditor::updateConfig()
 {
     updateEditConfig();
 }
+
+void VEditor::setVimMode(VimMode p_mode)
+{
+    if (m_editOps) {
+        m_editOps->setVimMode(p_mode);
+    }
+}

+ 3 - 0
src/veditor.h

@@ -16,6 +16,7 @@ class VEditOperations;
 class QTimer;
 class QLabel;
 class VVim;
+enum class VimMode;
 
 
 enum class SelectionId {
@@ -136,6 +137,8 @@ public:
     // Update config according to global configurations.
     virtual void updateConfig();
 
+    void setVimMode(VimMode p_mode);
+
 // Wrapper functions for QPlainTextEdit/QTextEdit.
 // Ends with W to distinguish it from the original interfaces.
 public:

+ 5 - 0
src/vedittab.cpp

@@ -124,3 +124,8 @@ bool VEditTab::tabHasFocus() const
 void VEditTab::insertLink()
 {
 }
+
+void VEditTab::applySnippet(const VSnippet *p_snippet)
+{
+    Q_UNUSED(p_snippet);
+}

+ 4 - 0
src/vedittab.h

@@ -10,6 +10,7 @@
 #include "vedittabinfo.h"
 
 class VEditArea;
+class VSnippet;
 
 // VEditTab is the base class of an edit tab inside VEditWindow.
 class VEditTab : public QWidget
@@ -91,6 +92,9 @@ public:
     // Called by evaluateMagicWordsByCaptain() to evaluate the magic words.
     virtual void evaluateMagicWords();
 
+    // Insert snippet @p_snippet.
+    virtual void applySnippet(const VSnippet *p_snippet);
+
 public slots:
     // Enter edit mode
     virtual void editFile() = 0;

+ 11 - 115
src/vfilelist.cpp

@@ -15,6 +15,7 @@
 #include "dialog/vconfirmdeletiondialog.h"
 #include "dialog/vsortdialog.h"
 #include "vmainwindow.h"
+#include "utils/vimnavigationforwidget.h"
 
 extern VConfigManager *g_config;
 extern VNote *g_vnote;
@@ -825,50 +826,21 @@ void VFileList::pasteFiles(VDirectory *p_destDir,
     getNewMagic();
 }
 
-void VFileList::keyPressEvent(QKeyEvent *event)
+void VFileList::keyPressEvent(QKeyEvent *p_event)
 {
-    int key = event->key();
-    int modifiers = event->modifiers();
-    switch (key) {
-    case Qt::Key_Return:
-    {
+    if (VimNavigationForWidget::injectKeyPressEventForVim(fileList,
+                                                          p_event)) {
+        return;
+    }
+
+    if (p_event->key() == Qt::Key_Return) {
         QListWidgetItem *item = fileList->currentItem();
         if (item) {
             handleItemClicked(item);
         }
-
-        break;
-    }
-
-
-    case Qt::Key_J:
-    {
-        if (modifiers == Qt::ControlModifier) {
-            event->accept();
-            QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down,
-                                                 Qt::NoModifier);
-            QCoreApplication::postEvent(fileList, downEvent);
-            return;
-        }
-        break;
     }
 
-    case Qt::Key_K:
-    {
-        if (modifiers == Qt::ControlModifier) {
-            event->accept();
-            QKeyEvent *upEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up,
-                                               Qt::NoModifier);
-            QCoreApplication::postEvent(fileList, upEvent);
-            return;
-        }
-        break;
-    }
-
-    default:
-        break;
-    }
-    QWidget::keyPressEvent(event);
+    QWidget::keyPressEvent(p_event);
 }
 
 void VFileList::focusInEvent(QFocusEvent * /* p_event */)
@@ -893,91 +865,15 @@ bool VFileList::locateFile(const VNoteFile *p_file)
     return false;
 }
 
-void VFileList::registerNavigation(QChar p_majorKey)
-{
-    m_majorKey = p_majorKey;
-    V_ASSERT(m_keyMap.empty());
-    V_ASSERT(m_naviLabels.empty());
-}
-
 void VFileList::showNavigation()
 {
-    // Clean up.
-    m_keyMap.clear();
-    for (auto label : m_naviLabels) {
-        delete label;
-    }
-    m_naviLabels.clear();
-
-    if (!isVisible()) {
-        return;
-    }
-
-    // Generate labels for visible items.
-    auto items = getVisibleItems();
-    int itemWidth = rect().width();
-    for (int i = 0; i < 26 && i < items.size(); ++i) {
-        QChar key('a' + i);
-        m_keyMap[key] = items[i];
-
-        QString str = QString(m_majorKey) + key;
-        QLabel *label = new QLabel(str, this);
-        label->setStyleSheet(g_vnote->getNavigationLabelStyle(str));
-        label->show();
-        QRect rect = fileList->visualItemRect(items[i]);
-        // Display the label at the end to show the file name.
-        label->move(rect.x() + itemWidth - label->width(), rect.y());
-        m_naviLabels.append(label);
-    }
-}
-
-void VFileList::hideNavigation()
-{
-    m_keyMap.clear();
-    for (auto label : m_naviLabels) {
-        delete label;
-    }
-    m_naviLabels.clear();
+    VNavigationMode::showNavigation(fileList);
 }
 
 bool VFileList::handleKeyNavigation(int p_key, bool &p_succeed)
 {
     static bool secondKey = false;
-    bool ret = false;
-    p_succeed = false;
-    QChar keyChar = VUtils::keyToChar(p_key);
-    if (secondKey && !keyChar.isNull()) {
-        secondKey = false;
-        p_succeed = true;
-        ret = true;
-        auto it = m_keyMap.find(keyChar);
-        if (it != m_keyMap.end()) {
-            fileList->setCurrentItem(it.value(), QItemSelectionModel::ClearAndSelect);
-            fileList->setFocus();
-        }
-    } else if (keyChar == m_majorKey) {
-        // Major key pressed.
-        // Need second key if m_keyMap is not empty.
-        if (m_keyMap.isEmpty()) {
-            p_succeed = true;
-        } else {
-            secondKey = true;
-        }
-        ret = true;
-    }
-    return ret;
-}
-
-QList<QListWidgetItem *> VFileList::getVisibleItems() const
-{
-    QList<QListWidgetItem *> items;
-    for (int i = 0; i < fileList->count(); ++i) {
-        QListWidgetItem *item = fileList->item(i);
-        if (!item->isHidden()) {
-            items.append(item);
-        }
-    }
-    return items;
+    return VNavigationMode::handleKeyNavigation(fileList, secondKey, p_key, p_succeed);
 }
 
 int VFileList::getNewMagic()

+ 2 - 10
src/vfilelist.h

@@ -49,9 +49,7 @@ public:
     QWidget *getContentWidget() const;
 
     // Implementations for VNavigationMode.
-    void registerNavigation(QChar p_majorKey) Q_DECL_OVERRIDE;
     void showNavigation() Q_DECL_OVERRIDE;
-    void hideNavigation() Q_DECL_OVERRIDE;
     bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE;
 
 public slots:
@@ -104,7 +102,8 @@ private slots:
     void sortItems();
 
 protected:
-    void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE;
+    void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE;
+
     void focusInEvent(QFocusEvent *p_event) Q_DECL_OVERRIDE;
 
 private:
@@ -140,8 +139,6 @@ private:
 
     inline QPointer<VNoteFile> getVFile(QListWidgetItem *p_item) const;
 
-    QList<QListWidgetItem *> getVisibleItems() const;
-
     // Fill the info of @p_item according to @p_file.
     void fillItem(QListWidgetItem *p_item, const VNoteFile *p_file);
 
@@ -174,11 +171,6 @@ private:
     QAction *m_openLocationAct;
     QAction *m_sortAct;
 
-    // Navigation Mode.
-    // Map second key to QListWidgetItem.
-    QMap<QChar, QListWidgetItem *> m_keyMap;
-    QVector<QLabel *> m_naviLabels;
-
     static const QString c_infoShortcutSequence;
     static const QString c_copyShortcutSequence;
     static const QString c_cutShortcutSequence;

+ 14 - 3
src/vmainwindow.cpp

@@ -30,6 +30,7 @@
 #include "vbuttonwithwidget.h"
 #include "vattachmentlist.h"
 #include "vfilesessioninfo.h"
+#include "vsnippetlist.h"
 
 VMainWindow *g_mainWin;
 
@@ -111,6 +112,7 @@ void VMainWindow::registerCaptainAndNavigationTargets()
     m_captain->registerNavigationTarget(m_fileList);
     m_captain->registerNavigationTarget(editArea);
     m_captain->registerNavigationTarget(outline);
+    m_captain->registerNavigationTarget(m_snippetList);
 
     // Register Captain mode targets.
     m_captain->registerCaptainTarget(tr("AttachmentList"),
@@ -1185,7 +1187,6 @@ void VMainWindow::initDockWindows()
     toolDock = new QDockWidget(tr("Tools"), this);
     toolDock->setObjectName("ToolsDock");
     toolDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
-    toolBox = new QToolBox(this);
 
     // Outline tree.
     outline = new VOutline(this);
@@ -1196,8 +1197,18 @@ void VMainWindow::initDockWindows()
     connect(outline, &VOutline::outlineItemActivated,
             editArea, &VEditArea::scrollToHeader);
 
-    toolBox->addItem(outline, QIcon(":/resources/icons/outline.svg"), tr("Outline"));
-    toolDock->setWidget(toolBox);
+    // Snippets.
+    m_snippetList = new VSnippetList(this);
+
+    m_toolBox = new QToolBox(this);
+    m_toolBox->addItem(outline,
+                       QIcon(":/resources/icons/outline.svg"),
+                       tr("Outline"));
+    m_toolBox->addItem(m_snippetList,
+                       QIcon(":/resources/icons/snippets.svg"),
+                       tr("Snippets"));
+
+    toolDock->setWidget(m_toolBox);
     addDockWidget(Qt::RightDockWidgetArea, toolDock);
 
     QAction *toggleAct = toolDock->toggleViewAction();

+ 16 - 1
src/vmainwindow.h

@@ -37,6 +37,7 @@ class QSystemTrayIcon;
 class QShortcut;
 class VButtonWithWidget;
 class VAttachmentList;
+class VSnippetList;
 
 enum class PanelViewState
 {
@@ -87,6 +88,8 @@ public:
 
     VFile *getCurrentFile() const;
 
+    VEditTab *getCurrentTab() const;
+
 signals:
     // Emit when editor related configurations were changed by user.
     void editorConfigUpdated();
@@ -294,8 +297,15 @@ private:
     VEditArea *editArea;
 
     QDockWidget *toolDock;
-    QToolBox *toolBox;
+
+    // Tool box in the dock widget.
+    QToolBox *m_toolBox;
+
     VOutline *outline;
+
+    // View and manage snippets.
+    VSnippetList *m_snippetList;
+
     VAvatar *m_avatar;
     VFindReplaceDialog *m_findReplaceDialog;
     VVimIndicator *m_vimIndicator;
@@ -396,4 +406,9 @@ inline VFile *VMainWindow::getCurrentFile() const
     return m_curFile;
 }
 
+inline VEditTab *VMainWindow::getCurrentTab() const
+{
+    return m_curTab;
+}
+
 #endif // VMAINWINDOW_H

+ 0 - 1
src/vmdeditor.h

@@ -10,7 +10,6 @@
 #include "veditor.h"
 #include "vconfigmanager.h"
 #include "vtableofcontent.h"
-#include "veditoperations.h"
 #include "vconfigmanager.h"
 #include "utils/vutils.h"
 

+ 17 - 0
src/vmdtab.cpp

@@ -18,6 +18,7 @@
 #include "vwebview.h"
 #include "vmdeditor.h"
 #include "vmainwindow.h"
+#include "vsnippet.h"
 
 extern VMainWindow *g_mainWin;
 
@@ -724,3 +725,19 @@ void VMdTab::evaluateMagicWords()
         getEditor()->evaluateMagicWords();
     }
 }
+
+void VMdTab::applySnippet(const VSnippet *p_snippet)
+{
+    if (isEditMode()
+        && m_file->isModifiable()
+        && p_snippet->getType() == VSnippet::Type::PlainText) {
+        Q_ASSERT(m_editor);
+        QTextCursor cursor = m_editor->textCursor();
+        bool changed = p_snippet->apply(cursor);
+        if (changed) {
+            m_editor->setTextCursor(cursor);
+
+            m_editor->setVimMode(VimMode::Insert);
+        }
+    }
+}

+ 2 - 0
src/vmdtab.h

@@ -75,6 +75,8 @@ public:
     // Evaluate magic words.
     void evaluateMagicWords() Q_DECL_OVERRIDE;
 
+    void applySnippet(const VSnippet *p_snippet) Q_DECL_OVERRIDE;
+
 public slots:
     // Enter edit mode.
     void editFile() Q_DECL_OVERRIDE;

+ 202 - 0
src/vnavigationmode.cpp

@@ -0,0 +1,202 @@
+#include "vnavigationmode.h"
+
+#include <QDebug>
+#include <QLabel>
+#include <QListWidget>
+#include <QTreeWidget>
+
+#include "vnote.h"
+#include "utils/vutils.h"
+
+extern VNote *g_vnote;
+
+VNavigationMode::VNavigationMode()
+{
+}
+
+VNavigationMode::~VNavigationMode()
+{
+}
+
+void VNavigationMode::registerNavigation(QChar p_majorKey)
+{
+    m_majorKey = p_majorKey;
+    Q_ASSERT(m_keyMap.empty());
+    Q_ASSERT(m_naviLabels.empty());
+}
+
+void VNavigationMode::hideNavigation()
+{
+    clearNavigation();
+}
+
+void VNavigationMode::showNavigation(QListWidget *p_widget)
+{
+    clearNavigation();
+
+    if (!p_widget->isVisible()) {
+        return;
+    }
+
+    // Generate labels for visible items.
+    auto items = getVisibleItems(p_widget);
+    for (int i = 0; i < 26 && i < items.size(); ++i) {
+        QChar key('a' + i);
+        m_keyMap[key] = items[i];
+
+        QString str = QString(m_majorKey) + key;
+        QLabel *label = new QLabel(str, p_widget);
+        label->setStyleSheet(g_vnote->getNavigationLabelStyle(str));
+        label->show();
+        QRect rect = p_widget->visualItemRect(items[i]);
+        // Display the label at the end to show the file name.
+        label->move(rect.x() + p_widget->rect().width() - label->width() - 2,
+                    rect.y());
+        m_naviLabels.append(label);
+    }
+}
+
+QList<QListWidgetItem *> VNavigationMode::getVisibleItems(const QListWidget *p_widget) const
+{
+    QList<QListWidgetItem *> items;
+    for (int i = 0; i < p_widget->count(); ++i) {
+        QListWidgetItem *item = p_widget->item(i);
+        if (!item->isHidden()) {
+            items.append(item);
+        }
+    }
+
+    return items;
+}
+
+static QList<QTreeWidgetItem *> getVisibleChildItems(const QTreeWidgetItem *p_item)
+{
+    QList<QTreeWidgetItem *> items;
+    if (p_item && !p_item->isHidden() && p_item->isExpanded()) {
+        for (int i = 0; i < p_item->childCount(); ++i) {
+            QTreeWidgetItem *child = p_item->child(i);
+            if (!child->isHidden()) {
+                items.append(child);
+                if (child->isExpanded()) {
+                    items.append(getVisibleChildItems(child));
+                }
+            }
+        }
+    }
+
+    return items;
+}
+
+QList<QTreeWidgetItem *> VNavigationMode::getVisibleItems(const QTreeWidget *p_widget) const
+{
+    QList<QTreeWidgetItem *> items;
+    for (int i = 0; i < p_widget->topLevelItemCount(); ++i) {
+        QTreeWidgetItem *item = p_widget->topLevelItem(i);
+        if (!item->isHidden()) {
+            items.append(item);
+            if (item->isExpanded()) {
+                items.append(getVisibleChildItems(item));
+            }
+        }
+    }
+
+    return items;
+}
+
+bool VNavigationMode::handleKeyNavigation(QListWidget *p_widget,
+                                          bool &p_secondKey,
+                                          int p_key,
+                                          bool &p_succeed)
+{
+    bool ret = false;
+    p_succeed = false;
+    QChar keyChar = VUtils::keyToChar(p_key);
+    if (p_secondKey && !keyChar.isNull()) {
+        p_secondKey = false;
+        p_succeed = true;
+        ret = true;
+        auto it = m_keyMap.find(keyChar);
+        if (it != m_keyMap.end()) {
+            p_widget->setCurrentItem(static_cast<QListWidgetItem *>(it.value()),
+                                     QItemSelectionModel::ClearAndSelect);
+            p_widget->setFocus();
+        }
+    } else if (keyChar == m_majorKey) {
+        // Major key pressed.
+        // Need second key if m_keyMap is not empty.
+        if (m_keyMap.isEmpty()) {
+            p_succeed = true;
+        } else {
+            p_secondKey = true;
+        }
+
+        ret = true;
+    }
+
+    return ret;
+}
+
+void VNavigationMode::showNavigation(QTreeWidget *p_widget)
+{
+    clearNavigation();
+
+    if (!p_widget->isVisible()) {
+        return;
+    }
+
+    // Generate labels for visible items.
+    auto items = getVisibleItems(p_widget);
+    for (int i = 0; i < 26 && i < items.size(); ++i) {
+        QChar key('a' + i);
+        m_keyMap[key] = items[i];
+
+        QString str = QString(m_majorKey) + key;
+        QLabel *label = new QLabel(str, p_widget);
+        label->setStyleSheet(g_vnote->getNavigationLabelStyle(str));
+        label->move(p_widget->visualItemRect(items[i]).topLeft());
+        label->show();
+        m_naviLabels.append(label);
+    }
+}
+
+void VNavigationMode::clearNavigation()
+{
+    m_keyMap.clear();
+    for (auto label : m_naviLabels) {
+        delete label;
+    }
+
+    m_naviLabels.clear();
+}
+
+bool VNavigationMode::handleKeyNavigation(QTreeWidget *p_widget,
+                                          bool &p_secondKey,
+                                          int p_key,
+                                          bool &p_succeed)
+{
+    bool ret = false;
+    p_succeed = false;
+    QChar keyChar = VUtils::keyToChar(p_key);
+    if (p_secondKey && !keyChar.isNull()) {
+        p_secondKey = false;
+        p_succeed = true;
+        ret = true;
+        auto it = m_keyMap.find(keyChar);
+        if (it != m_keyMap.end()) {
+            p_widget->setCurrentItem(static_cast<QTreeWidgetItem *>(it.value()));
+            p_widget->setFocus();
+        }
+    } else if (keyChar == m_majorKey) {
+        // Major key pressed.
+        // Need second key if m_keyMap is not empty.
+        if (m_keyMap.isEmpty()) {
+            p_succeed = true;
+        } else {
+            p_secondKey = true;
+        }
+
+        ret = true;
+    }
+
+    return ret;
+}

+ 44 - 4
src/vnavigationmode.h

@@ -2,23 +2,63 @@
 #define VNAVIGATIONMODE_H
 
 #include <QChar>
+#include <QVector>
+#include <QMap>
+#include <QList>
+
+class QLabel;
+class QListWidget;
+class QListWidgetItem;
+class QTreeWidget;
+class QTreeWidgetItem;
+
 
 // Interface class for Navigation Mode in Captain Mode.
 class VNavigationMode
 {
 public:
-    VNavigationMode() {};
-    virtual ~VNavigationMode() {};
+    VNavigationMode();
+
+    virtual ~VNavigationMode();
+
+    virtual void registerNavigation(QChar p_majorKey);
 
-    virtual void registerNavigation(QChar p_majorKey) = 0;
     virtual void showNavigation() = 0;
-    virtual void hideNavigation() = 0;
+
+    virtual void hideNavigation();
+
     // Return true if this object could consume p_key.
     // p_succeed indicates whether the keys hit a target successfully.
     virtual bool handleKeyNavigation(int p_key, bool &p_succeed) = 0;
 
 protected:
+    void clearNavigation();
+
+    void showNavigation(QListWidget *p_widget);
+
+    void showNavigation(QTreeWidget *p_widget);
+
+    bool handleKeyNavigation(QListWidget *p_widget,
+                             bool &p_secondKey,
+                             int p_key,
+                             bool &p_succeed);
+
+    bool handleKeyNavigation(QTreeWidget *p_widget,
+                             bool &p_secondKey,
+                             int p_key,
+                             bool &p_succeed);
+
     QChar m_majorKey;
+
+    // Map second key to item.
+    QMap<QChar, void *> m_keyMap;
+
+    QVector<QLabel *> m_naviLabels;
+
+private:
+    QList<QListWidgetItem *> getVisibleItems(const QListWidget *p_widget) const;
+
+    QList<QTreeWidgetItem *> getVisibleItems(const QTreeWidget *p_widget) const;
 };
 
 #endif // VNAVIGATIONMODE_H

+ 6 - 0
src/vnote.qrc

@@ -135,5 +135,11 @@
         <file>resources/icons/link.svg</file>
         <file>resources/icons/code_block.svg</file>
         <file>resources/icons/manage_template.svg</file>
+        <file>resources/icons/snippets.svg</file>
+        <file>resources/icons/add_snippet.svg</file>
+        <file>resources/icons/locate_snippet.svg</file>
+        <file>resources/icons/delete_snippet.svg</file>
+        <file>resources/icons/snippet_info.svg</file>
+        <file>resources/icons/apply_snippet.svg</file>
     </qresource>
 </RCC>

+ 4 - 39
src/vnotebookselector.cpp

@@ -21,6 +21,7 @@
 #include "veditarea.h"
 #include "vnofocusitemdelegate.h"
 #include "vmainwindow.h"
+#include "utils/vimnavigationforwidget.h"
 
 extern VConfigManager *g_config;
 
@@ -590,45 +591,9 @@ bool VNotebookSelector::handleKeyNavigation(int p_key, bool &p_succeed)
 
 bool VNotebookSelector::handlePopupKeyPress(QKeyEvent *p_event)
 {
-    int key = p_event->key();
-    int modifiers = p_event->modifiers();
-    switch (key) {
-    case Qt::Key_BracketLeft:
-    {
-        if (modifiers == Qt::ControlModifier) {
-            p_event->accept();
-            hidePopup();
-            return true;
-        }
-        break;
-    }
-
-    case Qt::Key_J:
-    {
-        if (modifiers == Qt::ControlModifier) {
-            p_event->accept();
-            QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down,
-                                                 Qt::NoModifier);
-            QCoreApplication::postEvent(m_listWidget, downEvent);
-            return true;
-        }
-        break;
-    }
-
-    case Qt::Key_K:
-    {
-        if (modifiers == Qt::ControlModifier) {
-            p_event->accept();
-            QKeyEvent *upEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up,
-                                               Qt::NoModifier);
-            QCoreApplication::postEvent(m_listWidget, upEvent);
-            return true;
-        }
-        break;
-    }
-
-    default:
-        break;
+    if (VimNavigationForWidget::injectKeyPressEventForVim(m_listWidget,
+                                                          p_event)) {
+        return true;
     }
 
     return false;

+ 7 - 2
src/vopenedlistmenu.cpp

@@ -194,6 +194,7 @@ void VOpenedListMenu::keyPressEvent(QKeyEvent *p_event)
             hide();
             return;
         }
+
         break;
     }
 
@@ -201,7 +202,7 @@ void VOpenedListMenu::keyPressEvent(QKeyEvent *p_event)
     {
         m_cmdTimer->stop();
         m_cmdNum = 0;
-        if (modifiers == Qt::ControlModifier) {
+        if (VUtils::isControlModifierForVim(modifiers)) {
             QList<QAction *> acts = actions();
             if (acts.size() == 0) {
                 return;
@@ -230,6 +231,7 @@ void VOpenedListMenu::keyPressEvent(QKeyEvent *p_event)
             setActiveAction(act);
             return;
         }
+
         break;
     }
 
@@ -237,11 +239,12 @@ void VOpenedListMenu::keyPressEvent(QKeyEvent *p_event)
     {
         m_cmdTimer->stop();
         m_cmdNum = 0;
-        if (modifiers == Qt::ControlModifier) {
+        if (VUtils::isControlModifierForVim(modifiers)) {
             QList<QAction *> acts = actions();
             if (acts.size() == 0) {
                 return;
             }
+
             int idx = acts.size() - 1;
             QAction *act = activeAction();
             if (act) {
@@ -266,6 +269,7 @@ void VOpenedListMenu::keyPressEvent(QKeyEvent *p_event)
             setActiveAction(act);
             return;
         }
+
         break;
     }
 
@@ -274,6 +278,7 @@ void VOpenedListMenu::keyPressEvent(QKeyEvent *p_event)
         m_cmdNum = 0;
         break;
     }
+
     QMenu::keyPressEvent(p_event);
 }
 

+ 5 - 95
src/voutline.cpp

@@ -221,108 +221,18 @@ void VOutline::keyPressEvent(QKeyEvent *event)
     QTreeWidget::keyPressEvent(event);
 }
 
-void VOutline::registerNavigation(QChar p_majorKey)
-{
-    m_majorKey = p_majorKey;
-    V_ASSERT(m_keyMap.empty());
-    V_ASSERT(m_naviLabels.empty());
-}
-
 void VOutline::showNavigation()
 {
-    // Clean up.
-    m_keyMap.clear();
-    for (auto label : m_naviLabels) {
-        delete label;
-    }
-    m_naviLabels.clear();
-
-    if (!isVisible()) {
-        return;
-    }
-
-    // Generate labels for visible items.
-    auto items = getVisibleItems();
-    for (int i = 0; i < 26 && i < items.size(); ++i) {
-        QChar key('a' + i);
-        m_keyMap[key] = items[i];
-
-        QString str = QString(m_majorKey) + key;
-        QLabel *label = new QLabel(str, this);
-        label->setStyleSheet(g_vnote->getNavigationLabelStyle(str));
-        label->move(visualItemRect(items[i]).topLeft());
-        label->show();
-        m_naviLabels.append(label);
-    }
-}
-
-void VOutline::hideNavigation()
-{
-    m_keyMap.clear();
-    for (auto label : m_naviLabels) {
-        delete label;
-    }
-    m_naviLabels.clear();
+    VNavigationMode::showNavigation(this);
 }
 
 bool VOutline::handleKeyNavigation(int p_key, bool &p_succeed)
 {
     static bool secondKey = false;
-    bool ret = false;
-    p_succeed = false;
-    QChar keyChar = VUtils::keyToChar(p_key);
-    if (secondKey && !keyChar.isNull()) {
-        secondKey = false;
-        p_succeed = true;
-        ret = true;
-        auto it = m_keyMap.find(keyChar);
-        if (it != m_keyMap.end()) {
-            setCurrentItem(it.value());
-            setFocus();
-        }
-    } else if (keyChar == m_majorKey) {
-        // Major key pressed.
-        // Need second key if m_keyMap is not empty.
-        if (m_keyMap.isEmpty()) {
-            p_succeed = true;
-        } else {
-            secondKey = true;
-        }
-        ret = true;
-    }
-    return ret;
-}
-
-QList<QTreeWidgetItem *> VOutline::getVisibleItems() const
-{
-    QList<QTreeWidgetItem *> items;
-    for (int i = 0; i < topLevelItemCount(); ++i) {
-        QTreeWidgetItem *item = topLevelItem(i);
-        if (!item->isHidden()) {
-            items.append(item);
-            if (item->isExpanded()) {
-                items.append(getVisibleChildItems(item));
-            }
-        }
-    }
-    return items;
-}
-
-QList<QTreeWidgetItem *> VOutline::getVisibleChildItems(const QTreeWidgetItem *p_item) const
-{
-    QList<QTreeWidgetItem *> items;
-    if (p_item && !p_item->isHidden() && p_item->isExpanded()) {
-        for (int i = 0; i < p_item->childCount(); ++i) {
-            QTreeWidgetItem *child = p_item->child(i);
-            if (!child->isHidden()) {
-                items.append(child);
-                if (child->isExpanded()) {
-                    items.append(getVisibleChildItems(child));
-                }
-            }
-        }
-    }
-    return items;
+    return VNavigationMode::handleKeyNavigation(this,
+                                                secondKey,
+                                                p_key,
+                                                p_succeed);
 }
 
 const VTableOfContentItem *VOutline::getHeaderFromItem(QTreeWidgetItem *p_item) const

+ 0 - 10
src/voutline.h

@@ -19,9 +19,7 @@ public:
     VOutline(QWidget *parent = 0);
 
     // Implementations for VNavigationMode.
-    void registerNavigation(QChar p_majorKey) Q_DECL_OVERRIDE;
     void showNavigation() Q_DECL_OVERRIDE;
-    void hideNavigation() Q_DECL_OVERRIDE;
     bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE;
 
 signals:
@@ -64,9 +62,6 @@ private:
 
     bool selectHeaderOne(QTreeWidgetItem *p_item, const VHeaderPointer &p_header);
 
-    QList<QTreeWidgetItem *> getVisibleItems() const;
-    QList<QTreeWidgetItem *> getVisibleChildItems(const QTreeWidgetItem *p_item) const;
-
     // Fill the info of @p_item.
     void fillItem(QTreeWidgetItem *p_item, const VTableOfContentItem &p_header);
 
@@ -79,11 +74,6 @@ private:
 
     // When true, won't emit outlineItemActivated().
     bool m_muted;
-
-    // Navigation Mode.
-    // Map second key to QTreeWidgetItem.
-    QMap<QChar, QTreeWidgetItem *> m_keyMap;
-    QVector<QLabel *> m_naviLabels;
 };
 
 #endif // VOUTLINE_H

+ 207 - 0
src/vsnippet.cpp

@@ -0,0 +1,207 @@
+#include "vsnippet.h"
+
+#include <QObject>
+#include <QDebug>
+
+#include "vconstants.h"
+#include "utils/vutils.h"
+#include "utils/veditutils.h"
+#include "utils/vmetawordmanager.h"
+
+extern VMetaWordManager *g_mwMgr;
+
+const QString VSnippet::c_defaultCursorMark = "@@";
+
+const QString VSnippet::c_defaultSelectionMark = "$$";
+
+QVector<QChar> VSnippet::s_allShortcuts;
+
+VSnippet::VSnippet()
+    : m_type(Type::PlainText),
+      m_cursorMark(c_defaultCursorMark)
+{
+}
+
+VSnippet::VSnippet(const QString &p_name,
+                   Type p_type,
+                   const QString &p_content,
+                   const QString &p_cursorMark,
+                   const QString &p_selectionMark,
+                   QChar p_shortcut)
+    : m_name(p_name),
+      m_type(p_type),
+      m_content(p_content),
+      m_cursorMark(p_cursorMark),
+      m_selectionMark(p_selectionMark),
+      m_shortcut(p_shortcut)
+{
+    Q_ASSERT(m_selectionMark != m_cursorMark);
+}
+
+bool VSnippet::update(const QString &p_name,
+                      Type p_type,
+                      const QString &p_content,
+                      const QString &p_cursorMark,
+                      const QString &p_selectionMark,
+                      QChar p_shortcut)
+{
+    bool updated = false;
+    if (m_name != p_name) {
+        m_name = p_name;
+        updated = true;
+    }
+
+    if (m_type != p_type) {
+        m_type = p_type;
+        updated = true;
+    }
+
+    if (m_content != p_content) {
+        m_content = p_content;
+        updated = true;
+    }
+
+    if (m_cursorMark != p_cursorMark) {
+        m_cursorMark = p_cursorMark;
+        updated = true;
+    }
+
+    if (m_selectionMark != p_selectionMark) {
+        m_selectionMark = p_selectionMark;
+        updated = true;
+    }
+
+    if (m_shortcut != p_shortcut) {
+        m_shortcut = p_shortcut;
+        updated = true;
+    }
+
+    qDebug() << "snippet" << m_name << "updated" << updated;
+
+    return updated;
+}
+
+QString VSnippet::typeStr(VSnippet::Type p_type)
+{
+    switch (p_type) {
+    case Type::PlainText:
+        return QObject::tr("PlainText");
+
+    case Type::Html:
+        return QObject::tr("Html");
+
+    default:
+        return QObject::tr("Invalid");
+    }
+}
+
+QJsonObject VSnippet::toJson() const
+{
+    QJsonObject snip;
+    snip[SnippetConfig::c_name] = m_name;
+    snip[SnippetConfig::c_type] = (int)m_type;
+    snip[SnippetConfig::c_cursorMark] = m_cursorMark;
+    snip[SnippetConfig::c_selectionMark] = m_selectionMark;
+    snip[SnippetConfig::c_shortcut] = m_shortcut.isNull() ? "" : QString(m_shortcut);
+
+    return snip;
+}
+
+VSnippet VSnippet::fromJson(const QJsonObject &p_json)
+{
+    QChar shortcut;
+    QString shortcutStr = p_json[SnippetConfig::c_shortcut].toString();
+    if (!shortcutStr.isEmpty() && isValidShortcut(shortcutStr[0])) {
+        shortcut = shortcutStr[0];
+    }
+
+    VSnippet snip(p_json[SnippetConfig::c_name].toString(),
+                  static_cast<VSnippet::Type>(p_json[SnippetConfig::c_type].toInt()),
+                  "",
+                  p_json[SnippetConfig::c_cursorMark].toString(),
+                  p_json[SnippetConfig::c_selectionMark].toString(),
+                  shortcut);
+
+    return snip;
+}
+
+const QVector<QChar> &VSnippet::getAllShortcuts()
+{
+    if (s_allShortcuts.isEmpty()) {
+        // Init.
+        char ch = 'a';
+        while (true) {
+            s_allShortcuts.append(ch);
+            if (ch == 'z') {
+                break;
+            }
+
+            ch++;
+        }
+    }
+
+    return s_allShortcuts;
+}
+
+bool VSnippet::isValidShortcut(QChar p_char)
+{
+    if (p_char >= 'a' && p_char <= 'z') {
+        return true;
+    }
+
+    return false;
+}
+
+bool VSnippet::apply(QTextCursor &p_cursor) const
+{
+    p_cursor.beginEditBlock();
+    // Delete selected text.
+    QString selection = VEditUtils::selectedText(p_cursor);
+    p_cursor.removeSelectedText();
+
+    // Evaluate the content.
+    QString content = g_mwMgr->evaluate(m_content);
+
+    // Find the cursor mark and break the content.
+    QString secondPart;
+    if (!m_cursorMark.isEmpty()) {
+        QStringList parts = content.split(m_cursorMark, QString::SkipEmptyParts);
+        Q_ASSERT(parts.size() < 3);
+
+        content = parts[0];
+        if (parts.size() == 2) {
+            secondPart = parts[1];
+        }
+    }
+
+    // Replace the selection mark.
+    if (!m_selectionMark.isEmpty()) {
+        content.replace(m_selectionMark, selection);
+    }
+
+    int pos = p_cursor.position() + content.size();
+
+    if (!secondPart.isEmpty()) {
+        secondPart.replace(m_selectionMark, selection);
+        content += secondPart;
+    }
+
+    // Insert it.
+    switch (m_type) {
+    case Type::Html:
+        p_cursor.insertHtml(content);
+        // TODO: set the position of the cursor.
+        break;
+
+    case Type::PlainText:
+        V_FALLTHROUGH;
+
+    default:
+        p_cursor.insertText(content);
+        p_cursor.setPosition(pos);
+        break;
+    }
+
+    p_cursor.endEditBlock();
+    return true;
+}

+ 113 - 0
src/vsnippet.h

@@ -0,0 +1,113 @@
+#ifndef VSNIPPET_H
+#define VSNIPPET_H
+
+#include <QString>
+#include <QJsonObject>
+#include <QTextCursor>
+
+
+class VSnippet
+{
+public:
+    enum Type
+    {
+        PlainText = 0,
+        Html,
+        Invalid
+    };
+
+    VSnippet();
+
+    VSnippet(const QString &p_name,
+             Type p_type = Type::PlainText,
+             const QString &p_content = QString(),
+             const QString &p_cursorMark = c_defaultCursorMark,
+             const QString &p_selectionMark = c_defaultSelectionMark,
+             QChar p_shortcut = QChar());
+
+    // Return true if there is any update.
+    bool update(const QString &p_name,
+                Type p_type,
+                const QString &p_content,
+                const QString &p_cursorMark,
+                const QString &p_selectionMark,
+                QChar p_shortcut);
+
+    const QString &getName() const
+    {
+        return m_name;
+    }
+
+    VSnippet::Type getType() const
+    {
+        return m_type;
+    }
+
+    const QString &getCursorMark() const
+    {
+        return m_cursorMark;
+    }
+
+    const QString &getSelectionMark() const
+    {
+        return m_selectionMark;
+    }
+
+    const QString &getContent() const
+    {
+        return m_content;
+    }
+
+    QChar getShortcut() const
+    {
+        return m_shortcut;
+    }
+
+    void setContent(const QString &p_content)
+    {
+        m_content = p_content;
+    }
+
+    // Not including m_content.
+    QJsonObject toJson() const;
+
+    // Apply this snippet via @p_cursor.
+    bool apply(QTextCursor &p_cursor) const;
+
+    // Not including m_content.
+    static VSnippet fromJson(const QJsonObject &p_json);
+
+    static QString typeStr(VSnippet::Type p_type);
+
+    static const QVector<QChar> &getAllShortcuts();
+
+    static bool isValidShortcut(QChar p_char);
+
+private:
+    // File name in the snippet folder.
+    QString m_name;
+
+    Type m_type;
+
+    // Support magic word.
+    QString m_content;
+
+    // String in the content that mark the position of the cursor after insertion.
+    // If there is no such mark in the content, the cursor should be put at the
+    // end of the insertion.
+    QString m_cursorMark;
+
+    // Selection marks in the content will be replaced by selected text.
+    QString m_selectionMark;
+
+    // Shortcut to apply this snippet.
+    QChar m_shortcut;
+
+    static const QString c_defaultCursorMark;
+
+    static const QString c_defaultSelectionMark;
+
+    static QVector<QChar> s_allShortcuts;
+};
+
+#endif // VSNIPPET_H

+ 606 - 0
src/vsnippetlist.cpp

@@ -0,0 +1,606 @@
+#include "vsnippetlist.h"
+
+#include <QtWidgets>
+
+#include "vconfigmanager.h"
+#include "dialog/veditsnippetdialog.h"
+#include "utils/vutils.h"
+#include "utils/vimnavigationforwidget.h"
+#include "dialog/vsortdialog.h"
+#include "dialog/vconfirmdeletiondialog.h"
+#include "vmainwindow.h"
+
+extern VConfigManager *g_config;
+
+extern VMainWindow *g_mainWin;
+
+const QString VSnippetList::c_infoShortcutSequence = "F2";
+
+VSnippetList::VSnippetList(QWidget *p_parent)
+    : QWidget(p_parent)
+{
+    setupUI();
+
+    initShortcuts();
+
+    initActions();
+
+    if (!readSnippetsFromConfig()) {
+        VUtils::showMessage(QMessageBox::Warning,
+                            tr("Warning"),
+                            tr("Fail to read snippets from <span style=\"%1\">%2</span>.")
+                              .arg(g_config->c_dataTextStyle)
+                              .arg(g_config->getSnippetConfigFolder()),
+                            "",
+                            QMessageBox::Ok,
+                            QMessageBox::Ok,
+                            this);
+    }
+
+    updateContent();
+}
+
+void VSnippetList::setupUI()
+{
+    m_addBtn = new QPushButton(QIcon(":/resources/icons/add_snippet.svg"), "");
+    m_addBtn->setToolTip(tr("New Snippet"));
+    m_addBtn->setProperty("FlatBtn", true);
+    connect(m_addBtn, &QPushButton::clicked,
+            this, &VSnippetList::newSnippet);
+
+    m_locateBtn = new QPushButton(QIcon(":/resources/icons/locate_snippet.svg"), "");
+    m_locateBtn->setToolTip(tr("Open Folder"));
+    m_locateBtn->setProperty("FlatBtn", true);
+    connect(m_locateBtn, &QPushButton::clicked,
+            this, [this]() {
+                makeSureFolderExist();
+                QUrl url = QUrl::fromLocalFile(g_config->getSnippetConfigFolder());
+                QDesktopServices::openUrl(url);
+            });
+
+    m_numLabel = new QLabel();
+
+    QHBoxLayout *btnLayout = new QHBoxLayout;
+    btnLayout->addWidget(m_addBtn);
+    btnLayout->addWidget(m_locateBtn);
+    btnLayout->addStretch();
+    btnLayout->addWidget(m_numLabel);
+    btnLayout->setContentsMargins(0, 0, 3, 0);
+
+    m_snippetList = new QListWidget();
+    m_snippetList->setContextMenuPolicy(Qt::CustomContextMenu);
+    m_snippetList->setSelectionMode(QAbstractItemView::ExtendedSelection);
+    m_snippetList->setEditTriggers(QAbstractItemView::SelectedClicked);
+    connect(m_snippetList, &QListWidget::customContextMenuRequested,
+            this, &VSnippetList::handleContextMenuRequested);
+    connect(m_snippetList, &QListWidget::itemActivated,
+            this, &VSnippetList::handleItemActivated);
+
+    QVBoxLayout *mainLayout = new QVBoxLayout();
+    mainLayout->addLayout(btnLayout);
+    mainLayout->addWidget(m_snippetList);
+    mainLayout->setContentsMargins(0, 0, 0, 0);
+
+    setLayout(mainLayout);
+}
+
+void VSnippetList::initActions()
+{
+    m_applyAct = new QAction(QIcon(":/resources/icons/apply_snippet.svg"),
+                             tr("&Apply"),
+                             this);
+    m_applyAct->setToolTip(tr("Insert this snippet in editor"));
+    connect(m_applyAct, &QAction::triggered,
+            this, [this]() {
+                QListWidgetItem *item = m_snippetList->currentItem();
+                handleItemActivated(item);
+            });
+
+    m_infoAct = new QAction(QIcon(":/resources/icons/snippet_info.svg"),
+                            tr("&Info\t%1").arg(VUtils::getShortcutText(c_infoShortcutSequence)),
+                            this);
+    m_infoAct->setToolTip(tr("View and edit snippet's information"));
+    connect(m_infoAct, &QAction::triggered,
+            this, &VSnippetList::snippetInfo);
+
+    m_deleteAct = new QAction(QIcon(":/resources/icons/delete_snippet.svg"),
+                              tr("&Delete"),
+                              this);
+    m_deleteAct->setToolTip(tr("Delete selected snippets"));
+    connect(m_deleteAct, &QAction::triggered,
+            this, &VSnippetList::deleteSelectedItems);
+
+    m_sortAct = new QAction(QIcon(":/resources/icons/sort.svg"),
+                            tr("&Sort"),
+                            this);
+    m_sortAct->setToolTip(tr("Sort snippets manually"));
+    connect(m_sortAct, &QAction::triggered,
+            this, &VSnippetList::sortItems);
+}
+
+void VSnippetList::initShortcuts()
+{
+    QShortcut *infoShortcut = new QShortcut(QKeySequence(c_infoShortcutSequence), this);
+    infoShortcut->setContext(Qt::WidgetWithChildrenShortcut);
+    connect(infoShortcut, &QShortcut::activated,
+            this, &VSnippetList::snippetInfo);
+}
+
+void VSnippetList::newSnippet()
+{
+    QString defaultName = VUtils::getFileNameWithSequence(g_config->getSnippetConfigFolder(),
+                                                          "snippet");
+
+    VSnippet tmpSnippet(defaultName);
+    QString info = tr("Magic words are supported in the content of the snippet.");
+    VEditSnippetDialog dialog(tr("Create Snippet"),
+                              info,
+                              m_snippets,
+                              tmpSnippet,
+                              this);
+    if (dialog.exec() == QDialog::Accepted) {
+        makeSureFolderExist();
+        VSnippet snippet(dialog.getNameInput(),
+                         dialog.getTypeInput(),
+                         dialog.getContentInput(),
+                         dialog.getCursorMarkInput(),
+                         dialog.getSelectionMarkInput());
+
+        QString errMsg;
+        if (!addSnippet(snippet, &errMsg)) {
+            VUtils::showMessage(QMessageBox::Warning,
+                                tr("Warning"),
+                                tr("Fail to create snippet <span style=\"%1\">%2</span>.")
+                                  .arg(g_config->c_dataTextStyle)
+                                  .arg(snippet.getName()),
+                                errMsg,
+                                QMessageBox::Ok,
+                                QMessageBox::Ok,
+                                this);
+
+            updateContent();
+        }
+    }
+}
+
+void VSnippetList::handleContextMenuRequested(QPoint p_pos)
+{
+    QListWidgetItem *item = m_snippetList->itemAt(p_pos);
+    QMenu menu(this);
+    menu.setToolTipsVisible(true);
+
+    if (item) {
+        int itemCount = m_snippetList->selectedItems().size();
+        if (itemCount == 1) {
+            menu.addAction(m_applyAct);
+            menu.addAction(m_infoAct);
+        }
+
+        menu.addAction(m_deleteAct);
+    }
+
+    m_snippetList->update();
+
+    if (m_snippets.size() > 1) {
+        if (!menu.actions().isEmpty()) {
+            menu.addSeparator();
+        }
+
+        menu.addAction(m_sortAct);
+    }
+
+    if (!menu.actions().isEmpty()) {
+        menu.exec(m_snippetList->mapToGlobal(p_pos));
+    }
+}
+
+void VSnippetList::handleItemActivated(QListWidgetItem *p_item)
+{
+    const VSnippet *snip = getSnippet(p_item);
+    if (snip) {
+        VEditTab *tab = g_mainWin->getCurrentTab();
+        if (tab) {
+            tab->applySnippet(snip);
+        }
+    }
+}
+
+void VSnippetList::deleteSelectedItems()
+{
+    QVector<ConfirmItemInfo> items;
+    const QList<QListWidgetItem *> selectedItems = m_snippetList->selectedItems();
+
+    if (selectedItems.isEmpty()) {
+        return;
+    }
+
+    for (auto const & item : selectedItems) {
+        items.push_back(ConfirmItemInfo(item->text(),
+                                        item->text(),
+                                        "",
+                                        NULL));
+    }
+
+    QString text = tr("Are you sure to delete these snippets?");
+    QString info = tr("Click \"Cancel\" to leave them untouched.");
+    VConfirmDeletionDialog dialog(tr("Confirm Deleting Snippets"),
+                                  text,
+                                  info,
+                                  items,
+                                  false,
+                                  false,
+                                  false,
+                                  this);
+    if (dialog.exec()) {
+        items = dialog.getConfirmedItems();
+
+        QList<QString> names;
+        for (auto const & item : items) {
+            names.append(item.m_name);
+        }
+
+        QString errMsg;
+        if (!deleteSnippets(names, &errMsg)) {
+            VUtils::showMessage(QMessageBox::Warning,
+                                tr("Warning"),
+                                tr("Fail to delete snippets."),
+                                errMsg,
+                                QMessageBox::Ok,
+                                QMessageBox::Ok,
+                                this);
+        }
+
+        updateContent();
+    }
+}
+
+void VSnippetList::sortItems()
+{
+    if (m_snippets.size() < 2) {
+        return;
+    }
+
+    VSortDialog dialog(tr("Sort Snippets"),
+                       tr("Sort snippets in the configuration file."),
+                       this);
+    QTreeWidget *tree = dialog.getTreeWidget();
+    tree->clear();
+    tree->setColumnCount(1);
+    QStringList headers;
+    headers << tr("Name");
+    tree->setHeaderLabels(headers);
+
+    for (int i = 0; i < m_snippets.size(); ++i) {
+        QTreeWidgetItem *item = new QTreeWidgetItem(tree, QStringList(m_snippets[i].getName()));
+
+        item->setData(0, Qt::UserRole, i);
+    }
+
+    dialog.treeUpdated();
+
+    if (dialog.exec()) {
+        QVector<QVariant> data = dialog.getSortedData();
+        Q_ASSERT(data.size() == m_snippets.size());
+        QVector<int> sortedIdx(data.size(), -1);
+        for (int i = 0; i < data.size(); ++i) {
+            sortedIdx[i] = data[i].toInt();
+        }
+
+        QString errMsg;
+        if (!sortSnippets(sortedIdx, &errMsg)) {
+            VUtils::showMessage(QMessageBox::Warning,
+                                tr("Warning"),
+                                tr("Fail to sort snippets."),
+                                errMsg,
+                                QMessageBox::Ok,
+                                QMessageBox::Ok,
+                                this);
+        }
+
+        updateContent();
+    }
+}
+
+void VSnippetList::snippetInfo()
+{
+    if (m_snippetList->selectedItems().size() != 1) {
+        return;
+    }
+
+    QListWidgetItem *item = m_snippetList->currentItem();
+    VSnippet *snip = getSnippet(item);
+    if (!snip) {
+        return;
+    }
+
+    QString info = tr("Magic words are supported in the content of the snippet.");
+    VEditSnippetDialog dialog(tr("Snippet Information"),
+                              info,
+                              m_snippets,
+                              *snip,
+                              this);
+    if (dialog.exec() == QDialog::Accepted) {
+        QString errMsg;
+        bool ret = true;
+        if (snip->getName() != dialog.getNameInput()) {
+            // Delete the original snippet file.
+            if (!deleteSnippetFile(*snip, &errMsg)) {
+                ret = false;
+            }
+        }
+
+        if (snip->update(dialog.getNameInput(),
+                         dialog.getTypeInput(),
+                         dialog.getContentInput(),
+                         dialog.getCursorMarkInput(),
+                         dialog.getSelectionMarkInput(),
+                         dialog.getShortcutInput())) {
+            if (!writeSnippetFile(*snip, &errMsg)) {
+                ret = false;
+            }
+
+            if (!writeSnippetsToConfig()) {
+                VUtils::addErrMsg(&errMsg,
+                                  tr("Fail to write snippets configuration file."));
+                ret = false;
+            }
+
+            updateContent();
+        }
+
+        if (!ret) {
+            VUtils::showMessage(QMessageBox::Warning,
+                                tr("Warning"),
+                                tr("Fail to update information of snippet <span style=\"%1\">%2</span>.")
+                                  .arg(g_config->c_dataTextStyle)
+                                  .arg(snip->getName()),
+                                errMsg,
+                                QMessageBox::Ok,
+                                QMessageBox::Ok,
+                                this);
+        }
+    }
+}
+
+void VSnippetList::makeSureFolderExist() const
+{
+    QString path = g_config->getSnippetConfigFolder();
+    if (!QFileInfo::exists(path)) {
+        QDir dir;
+        dir.mkpath(path);
+    }
+}
+
+void VSnippetList::updateContent()
+{
+    m_snippetList->clear();
+
+    for (int i = 0; i < m_snippets.size(); ++i) {
+        const VSnippet &snip = m_snippets[i];
+        QListWidgetItem *item = new QListWidgetItem(snip.getName());
+        item->setToolTip(snip.getName());
+        item->setData(Qt::UserRole, snip.getName());
+
+        m_snippetList->addItem(item);
+    }
+
+    int cnt = m_snippetList->count();
+    if (cnt > 0) {
+        m_numLabel->setText(tr("%1 %2").arg(cnt)
+                                       .arg(cnt > 1 ? tr("Snippets") : tr("Snippet")));
+        m_snippetList->setFocus();
+    } else {
+        m_numLabel->setText("");
+        m_addBtn->setFocus();
+    }
+}
+
+bool VSnippetList::addSnippet(const VSnippet &p_snippet, QString *p_errMsg)
+{
+    if (!writeSnippetFile(p_snippet, p_errMsg)) {
+        return false;
+    }
+
+    m_snippets.push_back(p_snippet);
+
+    bool ret = true;
+    if (!writeSnippetsToConfig()) {
+        VUtils::addErrMsg(p_errMsg,
+                          tr("Fail to write snippets configuration file."));
+        m_snippets.pop_back();
+        ret = false;
+    }
+
+    updateContent();
+
+    return ret;
+}
+
+bool VSnippetList::writeSnippetsToConfig() const
+{
+    makeSureFolderExist();
+
+    QJsonObject snippetJson;
+    snippetJson[SnippetConfig::c_version] = "1";
+
+    QJsonArray snippetArray;
+    for (int i = 0; i < m_snippets.size(); ++i) {
+        snippetArray.append(m_snippets[i].toJson());
+    }
+
+    snippetJson[SnippetConfig::c_snippets] = snippetArray;
+
+    return VUtils::writeJsonToDisk(g_config->getSnippetConfigFilePath(),
+                                   snippetJson);
+}
+
+bool VSnippetList::readSnippetsFromConfig()
+{
+    m_snippets.clear();
+
+    if (!QFileInfo::exists(g_config->getSnippetConfigFilePath())) {
+        return true;
+    }
+
+    QJsonObject snippets = VUtils::readJsonFromDisk(g_config->getSnippetConfigFilePath());
+    if (snippets.isEmpty()) {
+        qWarning() << "invalid snippets configuration file" << g_config->getSnippetConfigFilePath();
+        return false;
+    }
+
+    // [snippets] section.
+    bool ret = true;
+    QJsonArray snippetArray = snippets[SnippetConfig::c_snippets].toArray();
+    for (int i = 0; i < snippetArray.size(); ++i) {
+        VSnippet snip = VSnippet::fromJson(snippetArray[i].toObject());
+
+        // Read the content.
+        QString filePath(QDir(g_config->getSnippetConfigFolder()).filePath(snip.getName()));
+        QString content = VUtils::readFileFromDisk(filePath);
+        if (content.isNull()) {
+            qWarning() << "fail to read snippet" << snip.getName();
+            ret = false;
+            continue;
+        }
+
+        snip.setContent(content);
+        m_snippets.push_back(snip);
+    }
+
+    return ret;
+}
+
+void VSnippetList::keyPressEvent(QKeyEvent *p_event)
+{
+    if (VimNavigationForWidget::injectKeyPressEventForVim(m_snippetList,
+                                                          p_event)) {
+        return;
+    }
+
+    QWidget::keyPressEvent(p_event);
+}
+
+void VSnippetList::showNavigation()
+{
+    VNavigationMode::showNavigation(m_snippetList);
+}
+
+bool VSnippetList::handleKeyNavigation(int p_key, bool &p_succeed)
+{
+    static bool secondKey = false;
+    return VNavigationMode::handleKeyNavigation(m_snippetList,
+                                                secondKey,
+                                                p_key,
+                                                p_succeed);
+}
+
+int VSnippetList::getSnippetIndex(QListWidgetItem *p_item) const
+{
+    if (!p_item) {
+        return -1;
+    }
+
+    QString name = p_item->data(Qt::UserRole).toString();
+    for (int i = 0; i < m_snippets.size(); ++i) {
+        if (m_snippets[i].getName() == name) {
+            return i;
+        }
+    }
+
+    Q_ASSERT(false);
+    return -1;
+}
+
+VSnippet *VSnippetList::getSnippet(QListWidgetItem *p_item)
+{
+    int idx = getSnippetIndex(p_item);
+    if (idx == -1) {
+        return NULL;
+    } else {
+        return &m_snippets[idx];
+    }
+}
+
+bool VSnippetList::writeSnippetFile(const VSnippet &p_snippet, QString *p_errMsg)
+{
+    // Create and write to the snippet file.
+    QString filePath = getSnippetFilePath(p_snippet);
+    if (!VUtils::writeFileToDisk(filePath, p_snippet.getContent())) {
+        VUtils::addErrMsg(p_errMsg,
+                          tr("Fail to add write the snippet file %1.")
+                            .arg(filePath));
+        return false;
+    }
+
+    return true;
+}
+
+QString VSnippetList::getSnippetFilePath(const VSnippet &p_snippet) const
+{
+    return QDir(g_config->getSnippetConfigFolder()).filePath(p_snippet.getName());
+}
+
+bool VSnippetList::sortSnippets(const QVector<int> &p_sortedIdx, QString *p_errMsg)
+{
+    V_ASSERT(p_sortedIdx.size() == m_snippets.size());
+
+    auto ori = m_snippets;
+
+    for (int i = 0; i < p_sortedIdx.size(); ++i) {
+        m_snippets[i] = ori[p_sortedIdx[i]];
+    }
+
+    bool ret = true;
+    if (!writeSnippetsToConfig()) {
+        VUtils::addErrMsg(p_errMsg,
+                          tr("Fail to write snippets configuration file."));
+        m_snippets = ori;
+        ret = false;
+    }
+
+    return ret;
+}
+
+bool VSnippetList::deleteSnippets(const QList<QString> &p_snippets,
+                                  QString *p_errMsg)
+{
+    if (p_snippets.isEmpty()) {
+        return true;
+    }
+
+    bool ret = true;
+    QSet<QString> targets = QSet<QString>::fromList(p_snippets);
+    for (auto it = m_snippets.begin(); it != m_snippets.end();) {
+        if (targets.contains(it->getName())) {
+            // Remove it.
+            if (!deleteSnippetFile(*it, p_errMsg)) {
+                ret = false;
+            }
+
+            it = m_snippets.erase(it);
+        } else {
+            ++it;
+        }
+    }
+
+    if (!writeSnippetsToConfig()) {
+        VUtils::addErrMsg(p_errMsg,
+                          tr("Fail to write snippets configuration file."));
+        ret = false;
+    }
+
+    return ret;
+}
+
+bool VSnippetList::deleteSnippetFile(const VSnippet &p_snippet, QString *p_errMsg)
+{
+    QString filePath = getSnippetFilePath(p_snippet);
+    if (!VUtils::deleteFile(filePath)) {
+        VUtils::addErrMsg(p_errMsg,
+                          tr("Fail to remove snippet file %1.")
+                            .arg(filePath));
+        return false;
+    }
+
+    return true;
+}

+ 98 - 0
src/vsnippetlist.h

@@ -0,0 +1,98 @@
+#ifndef VSNIPPETLIST_H
+#define VSNIPPETLIST_H
+
+#include <QWidget>
+#include <QVector>
+#include <QPoint>
+
+#include "vsnippet.h"
+#include "vnavigationmode.h"
+
+class QPushButton;
+class QListWidget;
+class QListWidgetItem;
+class QLabel;
+class QAction;
+class QKeyEvent;
+
+
+class VSnippetList : public QWidget, public VNavigationMode
+{
+    Q_OBJECT
+public:
+    explicit VSnippetList(QWidget *p_parent = nullptr);
+
+    // Implementations for VNavigationMode.
+    void showNavigation() Q_DECL_OVERRIDE;
+    bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE;
+
+protected:
+    void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE;
+
+private slots:
+    void newSnippet();
+
+    void handleContextMenuRequested(QPoint p_pos);
+
+    void handleItemActivated(QListWidgetItem *p_item);
+
+    void snippetInfo();
+
+    void deleteSelectedItems();
+
+    void sortItems();
+
+private:
+    void setupUI();
+
+    void initActions();
+
+    void initShortcuts();
+
+    void makeSureFolderExist() const;
+
+    // Update list of snippets according to m_snippets.
+    void updateContent();
+
+    // Add @p_snippet.
+    bool addSnippet(const VSnippet &p_snippet, QString *p_errMsg = nullptr);
+
+    // Write m_snippets to config file.
+    bool writeSnippetsToConfig() const;
+
+    // Read from config file to m_snippets.
+    bool readSnippetsFromConfig();
+
+    // Get the snippet index in m_snippets of @p_item.
+    int getSnippetIndex(QListWidgetItem *p_item) const;
+
+    VSnippet *getSnippet(QListWidgetItem *p_item);
+
+    // Write the content of @p_snippet to file.
+    bool writeSnippetFile(const VSnippet &p_snippet, QString *p_errMsg = nullptr);
+
+    QString getSnippetFilePath(const VSnippet &p_snippet) const;
+
+    // Sort m_snippets according to @p_sortedIdx.
+    bool sortSnippets(const QVector<int> &p_sortedIdx, QString *p_errMsg = nullptr);
+
+    bool deleteSnippets(const QList<QString> &p_snippets, QString *p_errMsg = nullptr);
+
+    bool deleteSnippetFile(const VSnippet &p_snippet, QString *p_errMsg = nullptr);
+
+    QPushButton *m_addBtn;
+    QPushButton *m_locateBtn;
+    QLabel *m_numLabel;
+    QListWidget *m_snippetList;
+
+    QAction *m_applyAct;
+    QAction *m_infoAct;
+    QAction *m_deleteAct;
+    QAction *m_sortAct;
+
+    QVector<VSnippet> m_snippets;
+
+    static const QString c_infoShortcutSequence;
+};
+
+#endif // VSNIPPETLIST_H