Browse Source

VMdTab: live preview

Le Tan 7 years ago
parent
commit
c6b7561864

+ 1 - 1
src/dialog/vexportdialog.cpp

@@ -892,8 +892,8 @@ int VExportDialog::doExport(VCart *p_cart,
                             QString *p_errMsg,
                             QList<QString> *p_outputFiles)
 {
+    Q_UNUSED(p_cart);
     Q_ASSERT(p_cart);
-
     int ret = 0;
 
     QVector<QString> files = m_cart->getFiles();

+ 15 - 0
src/hgmarkdownhighlighter.h

@@ -46,12 +46,27 @@ struct HLUnitStyle
 // Fenced code block only.
 struct VCodeBlock
 {
+    // Global position of the start.
     int m_startPos;
+
     int m_startBlock;
     int m_endBlock;
+
     QString m_lang;
 
     QString m_text;
+
+    bool equalContent(const VCodeBlock &p_block) const
+    {
+        return p_block.m_lang == m_lang && p_block.m_text == m_text;
+    }
+
+    void updateNonContent(const VCodeBlock &p_block)
+    {
+        m_startPos = p_block.m_startPos;
+        m_startBlock = p_block.m_startBlock;
+        m_endBlock = p_block.m_endBlock;
+    }
 };
 
 // Highlight unit with global position and string style name.

+ 3 - 5
src/resources/hoedown.js

@@ -1,5 +1,3 @@
-var placeholder = document.getElementById('placeholder');
-
 // Use Marked to highlight code blocks in edit mode.
 marked.setOptions({
     highlight: function(code, lang) {
@@ -18,7 +16,7 @@ marked.setOptions({
 var updateHtml = function(html) {
     asyncJobsCount = 0;
 
-    placeholder.innerHTML = html;
+    contentDiv.innerHTML = html;
 
     insertImageCaption();
 
@@ -83,7 +81,7 @@ var updateHtml = function(html) {
     // MathJax may be not loaded for now.
     if (VEnableMathjax && (typeof MathJax != "undefined")) {
         try {
-            MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, postProcessMathJax]);
+            MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentDiv, postProcessMathJax]);
         } catch (err) {
             content.setLog("err: " + err);
             finishLogics();
@@ -100,7 +98,7 @@ var highlightText = function(text, id, timeStamp) {
 
 var textToHtml = function(text) {
     var html = marked(text);
-    var container = document.getElementById('text-to-html-div');
+    var container = textHtmlDiv;
     container.innerHTML = html;
 
     html = getHtmlWithInlineStyles(container);

+ 3 - 4
src/resources/markdown-it.js

@@ -1,4 +1,3 @@
-var placeholder = document.getElementById('placeholder');
 var nameCounter = 0;
 var toc = []; // Table of Content as a list
 
@@ -110,7 +109,7 @@ var updateText = function(text) {
 
     var needToc = mdHasTocSection(text);
     var html = markdownToHtml(text, needToc);
-    placeholder.innerHTML = html;
+    contentDiv.innerHTML = html;
     handleToc(needToc);
     insertImageCaption();
     renderMermaid('lang-mermaid');
@@ -124,7 +123,7 @@ var updateText = function(text) {
     // finishLoading logic.
     if (VEnableMathjax) {
         try {
-            MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, postProcessMathJax]);
+            MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentDiv, postProcessMathJax]);
         } catch (err) {
             content.setLog("err: " + err);
             finishLogics();
@@ -141,7 +140,7 @@ var highlightText = function(text, id, timeStamp) {
 
 var textToHtml = function(text) {
     var html = mdit.render(text);
-    var container = document.getElementById('text-to-html-div');
+    var container = textHtmlDiv;
     container.innerHTML = html;
 
     html = getHtmlWithInlineStyles(container);

+ 4 - 2
src/resources/markdown_template.html

@@ -30,8 +30,10 @@
     <script src="qrc:/resources/markdown_template.js" defer></script>
 </head>
 <body>
-    <div id="placeholder"></div>
+    <div id="content-div"></div>
 
-    <div id="text-to-html-div" style="display:none;"></div>
+    <div id="preview-div" style="display:none;"></div>
+
+    <div id="text-html-div" style="display:none;"></div>
 </body>
 </html>

+ 60 - 3
src/resources/markdown_template.js

@@ -1,5 +1,11 @@
 var channelInitialized = false;
 
+var contentDiv = document.getElementById('content-div');
+
+var previewDiv = document.getElementById('preview-div');
+
+var textHtmlDiv = document.getElementById('text-html-div');
+
 var content;
 
 // Current header index in all headers.
@@ -131,7 +137,7 @@ var styleContent = function() {
 };
 
 var htmlContent = function() {
-    content.htmlContentCB("", styleContent(), placeholder.innerHTML);
+    content.htmlContentCB("", styleContent(), contentDiv.innerHTML);
 };
 
 new QWebChannel(qt.webChannelTransport,
@@ -157,6 +163,11 @@ new QWebChannel(qt.webChannelTransport,
         content.plantUMLResultReady.connect(handlePlantUMLResult);
         content.graphvizResultReady.connect(handleGraphvizResult);
 
+        content.requestPreviewEnabled.connect(setPreviewEnabled);
+
+        content.requestPreviewCodeBlock.connect(previewCodeBlock);
+        content.requestSetPreviewContent.connect(setPreviewContent);
+
         if (typeof updateHtml == "function") {
             updateHtml(content.html);
             content.htmlChanged.connect(updateHtml);
@@ -980,7 +991,7 @@ window.onmousedown = function(e) {
     // Left button and Ctrl key.
     if (e.buttons == 1
         && e.ctrlKey
-        && window.getSelection().rangeCount == 0) {
+        && window.getSelection().type != 'Range') {
         vds_oriMouseClientX = e.clientX;
         vds_oriMouseClientY = e.clientY;
         vds_readyToScroll = true;
@@ -1267,7 +1278,7 @@ function getNodeText(el) {
 }
 
 var calculateWordCount = function() {
-    var words = getNodeText(placeholder);
+    var words = getNodeText(contentDiv);
 
     // Char without spaces.
     var cns = 0;
@@ -1349,3 +1360,49 @@ var handleGraphvizResult = function(id, format, result) {
 
     finishOneAsyncJob();
 };
+
+var setPreviewEnabled = function(enabled) {
+    if (enabled) {
+        contentDiv.style.display = 'none';
+        previewDiv.style.display = 'block';
+    } else {
+        contentDiv.style.display = 'block';
+        previewDiv.style.display = 'none';
+        previewDiv.innerHTML = '';
+    }
+};
+
+var previewCodeBlock = function(id, lang, text, isLivePreview) {
+    var div = previewDiv;
+    div.innerHTML = '';
+    div.className = '';
+
+    if (text.length == 0
+        || (lang != 'flow'
+            && lang != 'flowchart'
+            && lang != 'mermaid'
+            && (lang != 'puml' || VPlantUMLMode != 1))) {
+        return;
+    }
+
+    var pre = document.createElement('pre');
+    var code = document.createElement('code');
+    code.textContent = text;
+
+    pre.appendChild(code);
+    div.appendChild(pre);
+
+    if (lang == 'flow' || lang == 'flowchart') {
+        renderFlowchartOne(code);
+    } else if (lang == 'mermaid') {
+        renderMermaidOne(code);
+    } else if (lang == 'puml') {
+        renderPlantUMLOneOnline(code);
+    }
+};
+
+var setPreviewContent = function(lang, html) {
+    previewDiv.innerHTML = html;
+    // Treat plantUML and graphviz the same.
+    previewDiv.classList = VPlantUMLDivClass;
+};

+ 3 - 4
src/resources/marked.js

@@ -1,4 +1,3 @@
-var placeholder = document.getElementById('placeholder');
 var renderer = new marked.Renderer();
 var toc = []; // Table of contents as a list
 var nameCounter = 0;
@@ -54,7 +53,7 @@ var updateText = function(text) {
 
     var needToc = mdHasTocSection(text);
     var html = markdownToHtml(text, needToc);
-    placeholder.innerHTML = html;
+    contentDiv.innerHTML = html;
     handleToc(needToc);
     insertImageCaption();
     renderMermaid('lang-mermaid');
@@ -68,7 +67,7 @@ var updateText = function(text) {
     // finishLoading logic.
     if (VEnableMathjax) {
         try {
-            MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, postProcessMathJax]);
+            MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentDiv, postProcessMathJax]);
         } catch (err) {
             content.setLog("err: " + err);
             finishLogics();
@@ -85,7 +84,7 @@ var highlightText = function(text, id, timeStamp) {
 
 var textToHtml = function(text) {
     var html = marked(text);
-    var container = document.getElementById('text-to-html-div');
+    var container = textHtmlDiv;
     container.innerHTML = html;
 
     html = getHtmlWithInlineStyles(container);

+ 3 - 4
src/resources/showdown.js

@@ -1,4 +1,3 @@
-var placeholder = document.getElementById('placeholder');
 var renderer = new showdown.Converter({simplifiedAutoLink: 'true',
                                        excludeTrailingPunctuationFromURLs: 'true',
                                        strikethrough: 'true',
@@ -94,7 +93,7 @@ var updateText = function(text) {
 
     var needToc = mdHasTocSection(text);
     var html = markdownToHtml(text, needToc);
-    placeholder.innerHTML = html;
+    contentDiv.innerHTML = html;
     handleToc(needToc);
     insertImageCaption();
     highlightCodeBlocks(document,
@@ -114,7 +113,7 @@ var updateText = function(text) {
     // finishLoading logic.
     if (VEnableMathjax) {
         try {
-            MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, postProcessMathJax]);
+            MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentDiv, postProcessMathJax]);
         } catch (err) {
             content.setLog("err: " + err);
             finishLogics();
@@ -149,7 +148,7 @@ var textToHtml = function(text) {
 
     delete parser;
 
-    var container = document.getElementById('text-to-html-div');
+    var container = textHtmlDiv;
     container.innerHTML = html;
 
     html = getHtmlWithInlineStyles(container);

+ 4 - 0
src/resources/themes/v_moonlight/v_moonlight.css

@@ -307,3 +307,7 @@ table.hljs-ln tr td.hljs-ln-code {
     background-repeat: no-repeat;
     background-size: contain;
 }
+
+::selection {
+  background: #64B5F6;
+}

+ 4 - 0
src/resources/themes/v_native/v_native.css

@@ -237,3 +237,7 @@ table.hljs-ln tr td.hljs-ln-numbers {
 table.hljs-ln tr td.hljs-ln-code {
     padding-left: 10px;
 }
+
+::selection {
+  background: #64B5F6;
+}

+ 4 - 0
src/resources/themes/v_pure/v_pure.css

@@ -307,3 +307,7 @@ table.hljs-ln tr td.hljs-ln-code {
     background-repeat: no-repeat;
     background-size: contain;
 }
+
+::selection {
+  background: #64B5F6;
+}

+ 2 - 0
src/resources/vnote.ini

@@ -387,6 +387,8 @@ MagicWord=M
 ApplySnippet=S
 ; Open export dialog
 Export=O
+; Toggle live preview
+LivePreview=I
 
 [external_editors]
 ; Define external editors which could be called to edit notes

+ 4 - 2
src/src.pro

@@ -127,7 +127,8 @@ SOURCES += main.cpp\
     vlistfolderue.cpp \
     dialog/vfixnotebookdialog.cpp \
     vplantumlhelper.cpp \
-    vgraphvizhelper.cpp
+    vgraphvizhelper.cpp \
+    vlivepreviewhelper.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -245,7 +246,8 @@ HEADERS  += vmainwindow.h \
     vlistfolderue.h \
     dialog/vfixnotebookdialog.h \
     vplantumlhelper.h \
-    vgraphvizhelper.h
+    vgraphvizhelper.h \
+    vlivepreviewhelper.h
 
 RESOURCES += \
     vnote.qrc \

+ 18 - 0
src/vdocument.cpp

@@ -160,3 +160,21 @@ void VDocument::processGraphviz(int p_id, const QString &p_format, const QString
 
     m_graphvizHelper->processAsync(p_id, p_format, p_text);
 }
+
+void VDocument::setPreviewEnabled(bool p_enabled)
+{
+    emit requestPreviewEnabled(p_enabled);
+}
+
+void VDocument::previewCodeBlock(int p_id,
+                                 const QString &p_lang,
+                                 const QString &p_text,
+                                 bool p_livePreview)
+{
+    emit requestPreviewCodeBlock(p_id, p_lang, p_text, p_livePreview);
+}
+
+void VDocument::setPreviewContent(const QString &p_lang, const QString &p_html)
+{
+    emit requestSetPreviewContent(p_lang, p_html);
+}

+ 23 - 0
src/vdocument.h

@@ -47,6 +47,20 @@ public:
 
     const VWordCountInfo &getWordCountInfo() const;
 
+    // Whether change to preview mode.
+    void setPreviewEnabled(bool p_enabled);
+
+    // @p_livePreview: if true, display the result in the preview-div; otherwise,
+    // call previewCodeBlockCB() to pass back the result.
+    // Only for online parser.
+    void previewCodeBlock(int p_id,
+                          const QString &p_lang,
+                          const QString &p_text,
+                          bool p_livePreview);
+
+    // Set the content of the preview.
+    void setPreviewContent(const QString &p_lang, const QString &p_html);
+
 public slots:
     // Will be called in the HTML side
 
@@ -130,6 +144,15 @@ signals:
 
     void graphvizResultReady(int p_id, const QString &p_format, const QString &p_result);
 
+    void requestPreviewEnabled(bool p_enabled);
+
+    void requestPreviewCodeBlock(int p_id,
+                                 const QString &p_lang,
+                                 const QString &p_text,
+                                 bool p_livePreview);
+
+    void requestSetPreviewContent(const QString &p_lang, const QString &p_html);
+
 private:
     QString m_toc;
     QString m_header;

+ 17 - 0
src/veditarea.cpp

@@ -923,6 +923,10 @@ void VEditArea::registerCaptainTargets()
                                    g_config->getCaptainShortcutKeySequence("ApplySnippet"),
                                    this,
                                    applySnippetByCaptain);
+    captain->registerCaptainTarget(tr("LivePreview"),
+                                   g_config->getCaptainShortcutKeySequence("LivePreview"),
+                                   this,
+                                   toggleLivePreviewByCaptain);
 }
 
 bool VEditArea::activateTabByCaptain(void *p_target, void *p_data, int p_idx)
@@ -1085,6 +1089,19 @@ bool VEditArea::applySnippetByCaptain(void *p_target, void *p_data)
     return true;
 }
 
+bool VEditArea::toggleLivePreviewByCaptain(void *p_target, void *p_data)
+{
+    Q_UNUSED(p_data);
+    VEditArea *obj = static_cast<VEditArea *>(p_target);
+
+    VEditTab *tab = obj->getCurrentTab();
+    if (tab) {
+        tab->toggleLivePreview();
+    }
+
+    return true;
+}
+
 void VEditArea::recordClosedFile(const VFileSessionInfo &p_file)
 {
     for (auto it = m_lastClosedFiles.begin(); it != m_lastClosedFiles.end(); ++it) {

+ 3 - 0
src/veditarea.h

@@ -208,6 +208,9 @@ private:
     // Prompt for user to apply a snippet.
     static bool applySnippetByCaptain(void *p_target, void *p_data);
 
+    // Toggle live preview.
+    static bool toggleLivePreviewByCaptain(void *p_target, void *p_data);
+
     // End Captain mode functions.
 
     int curWindowIndex;

+ 2 - 0
src/veditor.h

@@ -366,6 +366,8 @@ signals:
 
     void mouseReleased(QMouseEvent *p_event);
 
+    void cursorPositionChanged();
+
 private slots:
     // Timer for find-wrap label.
     void labelTimerTimeout()

+ 4 - 0
src/vedittab.h

@@ -118,6 +118,10 @@ public:
     // Fetch tab stat info.
     virtual VWordCountInfo fetchWordCountInfo(bool p_editMode) const;
 
+    virtual void toggleLivePreview()
+    {
+    }
+
 public slots:
     // Enter edit mode
     virtual void editFile() = 0;

+ 235 - 0
src/vlivepreviewhelper.cpp

@@ -0,0 +1,235 @@
+#include "vlivepreviewhelper.h"
+
+#include <QDebug>
+
+#include "veditor.h"
+#include "vdocument.h"
+#include "vconfigmanager.h"
+#include "vgraphvizhelper.h"
+#include "vplantumlhelper.h"
+
+extern VConfigManager *g_config;
+
+// Use the highest 4 bits (31-28) to indicate the lang.
+#define LANG_PREFIX_GRAPHVIZ 0x10000000UL
+#define LANG_PREFIX_PLANTUML 0x20000000UL
+#define LANG_PREFIX_MASK 0xf0000000UL
+
+// Use th 27th bit to indicate the preview type.
+#define TYPE_LIVE_PREVIEW 0x0UL
+#define TYPE_INPLACE_PREVIEW 0x08000000UL
+#define TYPE_MASK 0x08000000UL
+
+#define INDEX_MASK 0x00ffffffUL
+
+VLivePreviewHelper::VLivePreviewHelper(VEditor *p_editor,
+                                       VDocument *p_document,
+                                       QObject *p_parent)
+    : QObject(p_parent),
+      m_editor(p_editor),
+      m_document(p_document),
+      m_cbIndex(-1),
+      m_livePreviewEnabled(false),
+      m_graphvizHelper(NULL),
+      m_plantUMLHelper(NULL)
+{
+    connect(m_editor->object(), &VEditorObject::cursorPositionChanged,
+            this, &VLivePreviewHelper::handleCursorPositionChanged);
+
+    m_flowchartEnabled = g_config->getEnableFlowchart();
+    m_mermaidEnabled = g_config->getEnableMermaid();
+    m_plantUMLMode = g_config->getPlantUMLMode();
+    m_graphvizEnabled = g_config->getEnableGraphviz();
+}
+
+bool VLivePreviewHelper::isPreviewLang(const QString &p_lang) const
+{
+    return (m_flowchartEnabled && (p_lang == "flow" || p_lang == "flowchart"))
+           || (m_mermaidEnabled && p_lang == "mermaid")
+           || (m_plantUMLMode != PlantUMLMode::DisablePlantUML && p_lang == "puml")
+           || (m_graphvizEnabled && p_lang == "dot");
+}
+
+void VLivePreviewHelper::updateCodeBlocks(const QVector<VCodeBlock> &p_codeBlocks)
+{
+    if (!m_livePreviewEnabled) {
+        return;
+    }
+
+    int lastIndex = m_cbIndex;
+    m_cbIndex = -1;
+    int cursorBlock = m_editor->textCursorW().block().blockNumber();
+    int idx = 0;
+    bool needUpdate = true;
+    int nrCached = 0;
+    for (auto const & cb : p_codeBlocks) {
+        if (!isPreviewLang(cb.m_lang)) {
+            continue;
+        }
+
+        bool cached = false;
+        if (idx < m_codeBlocks.size()) {
+            CodeBlock &vcb = m_codeBlocks[idx];
+            if (vcb.m_codeBlock.equalContent(cb)) {
+                vcb.m_codeBlock.updateNonContent(cb);
+                cached = true;
+                ++nrCached;
+            } else {
+                vcb.m_codeBlock = cb;
+                vcb.m_cachedResult.clear();
+            }
+        } else {
+            m_codeBlocks.append(CodeBlock());
+            m_codeBlocks[idx].m_codeBlock = cb;
+        }
+
+        if (cb.m_startBlock <= cursorBlock && cb.m_endBlock >= cursorBlock) {
+            if (lastIndex == idx && cached) {
+                needUpdate = false;
+            }
+
+            m_cbIndex = idx;
+        }
+
+        ++idx;
+    }
+
+    m_codeBlocks.resize(idx);
+
+    qDebug() << "VLivePreviewHelper cache" << nrCached << "code blocks of" << m_codeBlocks.size();
+
+    if (needUpdate) {
+        updateLivePreview();
+    }
+}
+
+void VLivePreviewHelper::handleCursorPositionChanged()
+{
+    if (!m_livePreviewEnabled || m_codeBlocks.isEmpty()) {
+        return;
+    }
+
+    int cursorBlock = m_editor->textCursorW().block().blockNumber();
+
+    int left = 0, right = m_codeBlocks.size() - 1;
+    int mid = left;
+    while (left <= right) {
+        mid = (left + right) / 2;
+        const CodeBlock &cb = m_codeBlocks[mid];
+
+        if (cb.m_codeBlock.m_startBlock <= cursorBlock && cb.m_codeBlock.m_endBlock >= cursorBlock) {
+            break;
+        } else if (cb.m_codeBlock.m_startBlock > cursorBlock) {
+            right = mid - 1;
+        } else {
+            left = mid + 1;
+        }
+    }
+
+    if (left <= right) {
+        if (m_cbIndex != mid) {
+            m_cbIndex = mid;
+            updateLivePreview();
+        }
+    }
+}
+
+static QString removeFence(const QString &p_text)
+{
+    Q_ASSERT(p_text.startsWith("```") && p_text.endsWith("```"));
+    int idx = p_text.indexOf('\n') + 1;
+    return p_text.mid(idx, p_text.size() - idx - 3);
+}
+
+void VLivePreviewHelper::updateLivePreview()
+{
+    if (m_cbIndex < 0) {
+        return;
+    }
+
+    Q_ASSERT(!(m_cbIndex & ~INDEX_MASK));
+
+    const CodeBlock &cb = m_codeBlocks[m_cbIndex];
+    QString text = removeFence(cb.m_codeBlock.m_text);
+    qDebug() << "updateLivePreview" << m_cbIndex << cb.m_codeBlock.m_lang;
+
+    if (cb.m_codeBlock.m_lang == "dot") {
+        if (!m_graphvizHelper) {
+            m_graphvizHelper = new VGraphvizHelper(this);
+            connect(m_graphvizHelper, &VGraphvizHelper::resultReady,
+                    this, &VLivePreviewHelper::localAsyncResultReady);
+        }
+
+        if (cb.m_cachedResult.isEmpty()) {
+            m_graphvizHelper->processAsync(m_cbIndex | LANG_PREFIX_GRAPHVIZ | TYPE_LIVE_PREVIEW,
+                                           "svg",
+                                           text);
+        } else {
+            qDebug() << "use cached preview result of code block" << m_cbIndex;
+            m_document->setPreviewContent(cb.m_codeBlock.m_lang, cb.m_cachedResult);
+        }
+    } else if (cb.m_codeBlock.m_lang == "puml" && m_plantUMLMode == PlantUMLMode::LocalPlantUML) {
+        if (!m_plantUMLHelper) {
+            m_plantUMLHelper = new VPlantUMLHelper(this);
+            connect(m_plantUMLHelper, &VPlantUMLHelper::resultReady,
+                    this, &VLivePreviewHelper::localAsyncResultReady);
+        }
+
+        if (cb.m_cachedResult.isEmpty()) {
+            m_plantUMLHelper->processAsync(m_cbIndex | LANG_PREFIX_PLANTUML | TYPE_LIVE_PREVIEW,
+                                           "svg",
+                                           text);
+        } else {
+            qDebug() << "use cached preview result of code block" << m_cbIndex;
+            m_document->setPreviewContent(cb.m_codeBlock.m_lang, cb.m_cachedResult);
+        }
+    } else {
+        m_document->previewCodeBlock(m_cbIndex, cb.m_codeBlock.m_lang, text, true);
+    }
+}
+
+void VLivePreviewHelper::setLivePreviewEnabled(bool p_enabled)
+{
+    if (m_livePreviewEnabled == p_enabled) {
+        return;
+    }
+
+    m_livePreviewEnabled = p_enabled;
+    if (!m_livePreviewEnabled) {
+        m_cbIndex = -1;
+        m_codeBlocks.clear();
+        m_document->previewCodeBlock(-1, "", "", true);
+    }
+}
+
+void VLivePreviewHelper::localAsyncResultReady(int p_id,
+                                               const QString &p_format,
+                                               const QString &p_result)
+{
+    Q_UNUSED(p_format);
+    Q_ASSERT(p_format == "svg");
+    int idx = p_id & INDEX_MASK;
+    bool livePreview = (p_id & TYPE_MASK) == TYPE_LIVE_PREVIEW;
+    QString lang;
+    switch (p_id & LANG_PREFIX_MASK) {
+    case LANG_PREFIX_PLANTUML:
+        lang = "puml";
+        break;
+
+    case LANG_PREFIX_GRAPHVIZ:
+        lang = "dot";
+        break;
+
+    default:
+        return;
+    }
+
+    if (livePreview) {
+        if (idx != m_cbIndex) {
+            return;
+        }
+
+        m_codeBlocks[idx].m_cachedResult = p_result;
+        m_document->setPreviewContent(lang, p_result);
+    }
+}

+ 63 - 0
src/vlivepreviewhelper.h

@@ -0,0 +1,63 @@
+#ifndef VLIVEPREVIEWHELPER_H
+#define VLIVEPREVIEWHELPER_H
+
+#include <QObject>
+
+#include "hgmarkdownhighlighter.h"
+
+class VEditor;
+class VDocument;
+class VGraphvizHelper;
+class VPlantUMLHelper;
+
+class VLivePreviewHelper : public QObject
+{
+    Q_OBJECT
+public:
+    VLivePreviewHelper(VEditor *p_editor,
+                       VDocument *p_document,
+                       QObject *p_parent = nullptr);
+
+    void updateLivePreview();
+
+    void setLivePreviewEnabled(bool p_enabled);
+
+public slots:
+    void updateCodeBlocks(const QVector<VCodeBlock> &p_codeBlocks);
+
+private slots:
+    void handleCursorPositionChanged();
+
+    void localAsyncResultReady(int p_id, const QString &p_format, const QString &p_result);
+
+private:
+    bool isPreviewLang(const QString &p_lang) const;
+
+    struct CodeBlock
+    {
+        VCodeBlock m_codeBlock;
+        QString m_cachedResult;
+    };
+
+    // Sorted by m_startBlock in ascending order.
+    QVector<CodeBlock> m_codeBlocks;
+
+    VEditor *m_editor;
+
+    VDocument *m_document;
+
+    // Current previewed code block index in m_codeBlocks.
+    int m_cbIndex;
+
+    bool m_flowchartEnabled;
+    bool m_mermaidEnabled;
+    int m_plantUMLMode;
+    bool m_graphvizEnabled;
+
+    bool m_livePreviewEnabled;
+
+    VGraphvizHelper *m_graphvizHelper;
+    VPlantUMLHelper *m_plantUMLHelper;
+};
+
+#endif // VLIVEPREVIEWHELPER_H

+ 22 - 6
src/vmdeditor.cpp

@@ -38,7 +38,8 @@ VMdEditor::VMdEditor(VFile *p_file,
       m_mdHighlighter(NULL),
       m_freshEdit(true),
       m_textToHtmlDialog(NULL),
-      m_zoomDelta(0)
+      m_zoomDelta(0),
+      m_editTab(NULL)
 {
     Q_ASSERT(p_file->getDocType() == DocType::Markdown);
 
@@ -97,6 +98,9 @@ VMdEditor::VMdEditor(VFile *p_file,
     connect(this, &VTextEdit::cursorPositionChanged,
             this, &VMdEditor::updateCurrentHeader);
 
+    connect(this, &VTextEdit::cursorPositionChanged,
+            m_object, &VEditorObject::cursorPositionChanged);
+
     setDisplayScaleFactor(VUtils::calculateScaleFactor());
 
     updateFontAndPalette();
@@ -276,10 +280,7 @@ void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event)
 {
     QScopedPointer<QMenu> menu(createStandardContextMenu());
     menu->setToolTipsVisible(true);
-
-    VEditTab *editTab = dynamic_cast<VEditTab *>(parent());
-    Q_ASSERT(editTab);
-    if (editTab->isEditMode()) {
+    if (m_editTab && m_editTab->isEditMode()) {
         const QList<QAction *> actions = menu->actions();
 
         if (textCursor().hasSelection()) {
@@ -303,9 +304,19 @@ void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event)
                         emit m_object->discardAndRead();
                     });
 
-            menu->insertAction(actions.isEmpty() ? NULL : actions[0], discardExitAct);
+            QAction *toggleLivePreviewAct = new QAction(tr("Toggle Live Preview"), menu.data());
+            toggleLivePreviewAct->setToolTip(tr("Toggle live preview of diagrams"));
+            connect(toggleLivePreviewAct, &QAction::triggered,
+                    this, [this]() {
+                        m_editTab->toggleLivePreview();
+                    });
+
+            menu->insertAction(actions.isEmpty() ? NULL : actions[0], toggleLivePreviewAct);
+            menu->insertAction(toggleLivePreviewAct, discardExitAct);
             menu->insertAction(discardExitAct, saveExitAct);
 
+            menu->insertSeparator(toggleLivePreviewAct);
+
             if (!actions.isEmpty()) {
                 menu->insertSeparator(actions[0]);
             }
@@ -1322,3 +1333,8 @@ VWordCountInfo VMdEditor::fetchWordCountInfo() const
     info.m_charWithSpacesCount = cc;
     return info;
 }
+
+void VMdEditor::setEditTab(VEditTab *p_editTab)
+{
+    m_editTab = p_editTab;
+}

+ 12 - 0
src/vmdeditor.h

@@ -19,6 +19,7 @@ class VCodeBlockHighlightHelper;
 class VDocument;
 class VPreviewManager;
 class VCopyTextAsHtmlDialog;
+class VEditTab;
 
 class VMdEditor : public VTextEdit, public VEditor
 {
@@ -69,6 +70,10 @@ public:
 
     VWordCountInfo fetchWordCountInfo() const Q_DECL_OVERRIDE;
 
+    void setEditTab(VEditTab *p_editTab);
+
+    HGMarkdownHighlighter *getMarkdownHighlighter() const;
+
 public slots:
     bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE;
 
@@ -260,5 +265,12 @@ private:
     VCopyTextAsHtmlDialog *m_textToHtmlDialog;
 
     int m_zoomDelta;
+
+    VEditTab *m_editTab;
 };
+
+inline HGMarkdownHighlighter *VMdEditor::getMarkdownHighlighter() const
+{
+    return m_mdHighlighter;
+}
 #endif // VMDEDITOR_H

+ 81 - 8
src/vmdtab.cpp

@@ -22,11 +22,13 @@
 #include "vsnippet.h"
 #include "vinsertselector.h"
 #include "vsnippetlist.h"
+#include "vlivepreviewhelper.h"
 
 extern VMainWindow *g_mainWin;
 
 extern VConfigManager *g_config;
 
+
 VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea,
                OpenFileMode p_mode, QWidget *p_parent)
     : VEditTab(p_file, p_editArea, p_parent),
@@ -35,7 +37,9 @@ VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea,
       m_document(NULL),
       m_mdConType(g_config->getMdConverterType()),
       m_enableHeadingSequence(false),
-      m_backupFileChecked(false)
+      m_backupFileChecked(false),
+      m_mode(Mode::InvalidMode),
+      m_livePreviewHelper(NULL)
 {
     V_ASSERT(m_file->getDocType() == DocType::Markdown);
 
@@ -459,6 +463,7 @@ void VMdTab::setupMarkdownEditor()
 
     m_editor = new VMdEditor(m_file, m_document, m_mdConType, this);
     m_editor->setProperty("MainEditor", true);
+    m_editor->setEditTab(this);
     connect(m_editor, &VMdEditor::headersChanged,
             this, &VMdTab::updateOutlineFromHeaders);
     connect(m_editor, SIGNAL(currentHeaderChanged(int)),
@@ -467,10 +472,10 @@ void VMdTab::setupMarkdownEditor()
             this, &VMdTab::updateStatus);
     connect(m_editor, &VMdEditor::textChanged,
             this, &VMdTab::updateStatus);
-    connect(m_editor, &VMdEditor::cursorPositionChanged,
-            this, &VMdTab::updateCursorStatus);
     connect(g_mainWin, &VMainWindow::editorConfigUpdated,
             m_editor, &VMdEditor::updateConfig);
+    connect(m_editor->object(), &VEditorObject::cursorPositionChanged,
+            this, &VMdTab::updateCursorStatus);
     connect(m_editor->object(), &VEditorObject::saveAndRead,
             this, &VMdTab::saveAndRead);
     connect(m_editor->object(), &VEditorObject::discardAndRead,
@@ -753,9 +758,7 @@ void VMdTab::handleWebKeyPressed(int p_key, bool p_ctrl, bool p_shift)
 
 void VMdTab::zoom(bool p_zoomIn, qreal p_step)
 {
-    // Editor will handle it itself.
-    Q_ASSERT(!m_isEditMode);
-    if (!m_isEditMode) {
+    if (!m_isEditMode || m_mode == Mode::EditPreview) {
         zoomWebPage(p_zoomIn, p_step);
     }
 }
@@ -1316,6 +1319,18 @@ VWordCountInfo VMdTab::fetchWordCountInfo(bool p_editMode) const
 
 void VMdTab::setCurrentMode(Mode p_mode)
 {
+    if (m_mode == p_mode) {
+        return;
+    }
+
+    qreal factor = m_webViewer->zoomFactor();
+    if (m_mode == Mode::Read) {
+        m_readWebViewState->m_zoomFactor = factor;
+    } else if (m_mode == Mode::EditPreview) {
+        m_previewWebViewState->m_zoomFactor = factor;
+        m_livePreviewHelper->setLivePreviewEnabled(false);
+    }
+
     switch (p_mode) {
     case Mode::Read:
         m_webViewer->show();
@@ -1323,15 +1338,57 @@ void VMdTab::setCurrentMode(Mode p_mode)
             m_editor->hide();
         }
 
+        if (m_readWebViewState.isNull()) {
+            m_readWebViewState.reset(new WebViewState());
+            m_readWebViewState->m_zoomFactor = factor;
+        } else if (factor != m_readWebViewState->m_zoomFactor) {
+            m_webViewer->setZoomFactor(m_readWebViewState->m_zoomFactor);
+        }
+
+        m_document->setPreviewEnabled(false);
         break;
 
-    case Mode::EditPreview:
     case Mode::Edit:
-        Q_ASSERT(m_editor);
         m_editor->show();
         m_webViewer->hide();
         break;
 
+    case Mode::EditPreview:
+        Q_ASSERT(m_editor);
+        m_editor->show();
+        m_webViewer->show();
+        if (m_previewWebViewState.isNull()) {
+            m_previewWebViewState.reset(new WebViewState());
+            m_previewWebViewState->m_zoomFactor = factor;
+
+            // Init the size of two splits.
+            QList<int> sizes = m_splitter->sizes();
+            Q_ASSERT(sizes.size() == 2);
+            int a = (sizes[0] + sizes[1]) / 2;
+            if (a <= 0) {
+                a = 1;
+            }
+
+            int b = (sizes[0] + sizes[1]) - a;
+
+            QList<int> newSizes;
+            newSizes.append(a);
+            newSizes.append(b);
+            m_splitter->setSizes(newSizes);
+
+            Q_ASSERT(!m_livePreviewHelper);
+            m_livePreviewHelper = new VLivePreviewHelper(m_editor, m_document, this);
+            connect(m_editor->getMarkdownHighlighter(), &HGMarkdownHighlighter::codeBlocksUpdated,
+                    m_livePreviewHelper, &VLivePreviewHelper::updateCodeBlocks);
+        } else if (factor != m_previewWebViewState->m_zoomFactor) {
+            m_webViewer->setZoomFactor(m_previewWebViewState->m_zoomFactor);
+        }
+
+        m_document->setPreviewEnabled(true);
+        m_livePreviewHelper->setLivePreviewEnabled(true);
+        m_editor->getMarkdownHighlighter()->updateHighlight();
+        break;
+
     default:
         break;
     }
@@ -1340,3 +1397,19 @@ void VMdTab::setCurrentMode(Mode p_mode)
 
     focusChild();
 }
+
+void VMdTab::toggleLivePreview()
+{
+    switch (m_mode) {
+    case Mode::EditPreview:
+        setCurrentMode(Mode::Edit);
+        break;
+
+    case Mode::Edit:
+        setCurrentMode(Mode::EditPreview);
+        break;
+
+    default:
+        break;
+    }
+}

+ 16 - 1
src/vmdtab.h

@@ -3,6 +3,7 @@
 
 #include <QString>
 #include <QPointer>
+#include <QSharedPointer>
 #include "vedittab.h"
 #include "vconstants.h"
 #include "vmarkdownconverter.h"
@@ -15,6 +16,7 @@ class VInsertSelector;
 class QTimer;
 class QWebEngineDownloadItem;
 class QSplitter;
+class VLivePreviewHelper;
 
 class VMdTab : public VEditTab
 {
@@ -91,6 +93,9 @@ public:
     // Fetch tab stat info.
     VWordCountInfo fetchWordCountInfo(bool p_editMode) const Q_DECL_OVERRIDE;
 
+    // Toggle live preview in edit mode.
+    void toggleLivePreview() Q_DECL_OVERRIDE;
+
 public slots:
     // Enter edit mode.
     void editFile() Q_DECL_OVERRIDE;
@@ -145,7 +150,12 @@ private slots:
 private:
     enum TabReady { None = 0, ReadMode = 0x1, EditMode = 0x2 };
 
-    enum Mode { Read = 0, Edit, EditPreview };
+    enum Mode { InvalidMode = 0, Read, Edit, EditPreview };
+
+    struct WebViewState
+    {
+        qreal m_zoomFactor;
+    };
 
     // Setup UI.
     void setupUI();
@@ -238,6 +248,11 @@ private:
     VVim::SearchItem m_lastSearchItem;
 
     Mode m_mode;
+
+    QSharedPointer<WebViewState> m_readWebViewState;
+    QSharedPointer<WebViewState> m_previewWebViewState;
+
+    VLivePreviewHelper *m_livePreviewHelper;
 };
 
 inline VMdEditor *VMdTab::getEditor()