Browse Source

change HGMarkdownHighlighter to use QSyntaxHighlighter

Signed-off-by: Le Tan <[email protected]>
Le Tan 9 years ago
parent
commit
29609f0b65
7 changed files with 186 additions and 243 deletions
  1. 133 197
      hgmarkdownhighlighter.cpp
  2. 40 36
      hgmarkdownhighlighter.h
  3. 5 1
      vedit.cpp
  4. 3 0
      vedit.h
  5. 0 4
      veditor.cpp
  6. 0 2
      veditor.h
  7. 5 3
      vstyleparser.cpp

+ 133 - 197
hgmarkdownhighlighter.cpp

@@ -11,32 +11,9 @@
 #include <QtDebug>
 #include "hgmarkdownhighlighter.h"
 
-#ifndef QT_NO_DEBUG
-#define V_HIGHLIGHT_DEBUG
-#endif
+const int HGMarkdownHighlighter::initCapacity = 1024;
 
-const unsigned int WorkerThread::initCapacity = 1024;
-
-WorkerThread::WorkerThread()
-    : QThread(NULL), content(NULL), capacity(0), result(NULL)
-{
-    resizeBuffer(initCapacity);
-}
-
-WorkerThread::~WorkerThread()
-{
-    if (result) {
-        pmh_free_elements(result);
-        result = NULL;
-    }
-    if (content) {
-        delete [] content;
-        capacity = 0;
-        content = NULL;
-    }
-}
-
-void WorkerThread::resizeBuffer(unsigned int newCap)
+void HGMarkdownHighlighter::resizeBuffer(int newCap)
 {
     if (newCap == capacity) {
         return;
@@ -49,249 +26,208 @@ void WorkerThread::resizeBuffer(unsigned int newCap)
     content = new char [capacity];
 }
 
-void WorkerThread::prepareAndStart(const char *data)
-{
-    Q_ASSERT(data);
-    unsigned int len = strlen(data);
-    if (len >= capacity) {
-        resizeBuffer(qMax(2 * capacity, len + 1));
-    }
-    Q_ASSERT(content);
-    memcpy(content, data, len);
-    content[len] = '\0';
-
-    if (result) {
-        pmh_free_elements(result);
-        result = NULL;
+// Will be freeed by parent automatically
+HGMarkdownHighlighter::HGMarkdownHighlighter(const QVector<HighlightingStyle> &styles, int waitInterval,
+                                             QTextDocument *parent)
+    : QSyntaxHighlighter(parent), parsing(0),
+      waitInterval(waitInterval), content(NULL), capacity(0), result(NULL)
+{
+    codeBlockStartExp = QRegExp("^```");
+    codeBlockEndExp = QRegExp("^```$");
+    codeBlockFormat.setForeground(QBrush(Qt::darkYellow));
+    for (int index = 0; index < styles.size(); ++index) {
+        if (styles[index].type == pmh_VERBATIM) {
+            codeBlockFormat = styles[index].format;
+            break;
+        }
     }
 
-    start();
-}
-
-pmh_element** WorkerThread::retriveResult()
-{
-    Q_ASSERT(result);
-    pmh_element** ret = result;
-    result = NULL;
-    return ret;
-}
-
-void WorkerThread::run()
-{
-    if (content == NULL)
-        return;
-    Q_ASSERT(!result);
-    pmh_markdown_to_elements(content, pmh_EXT_NONE, &result);
-}
-
-// Will be freeed by parent automatically
-HGMarkdownHighlighter::HGMarkdownHighlighter(const QVector<HighlightingStyle> &styles,
-                                             QTextDocument *parent,
-                                             int aWaitInterval) : QObject(parent)
-{
-    workerThread = new WorkerThread();
-    cached_elements = NULL;
-    waitInterval = aWaitInterval;
+    resizeBuffer(initCapacity);
     setStyles(styles);
+    document = parent;
     timer = new QTimer(this);
     timer->setSingleShot(true);
-    timer->setInterval(aWaitInterval);
-    connect(timer, SIGNAL(timeout()), this, SLOT(timerTimeout()));
-    document = parent;
-    connect(document, SIGNAL(contentsChange(int,int,int)),
-            this, SLOT(handleContentsChange(int,int,int)));
-    connect(workerThread, SIGNAL(finished()), this, SLOT(threadFinished()));
-    this->parse();
+    timer->setInterval(this->waitInterval);
+    connect(timer, &QTimer::timeout, this, &HGMarkdownHighlighter::timerTimeout);
+    connect(document, &QTextDocument::contentsChange,
+            this, &HGMarkdownHighlighter::handleContentChange);
 }
 
 HGMarkdownHighlighter::~HGMarkdownHighlighter()
 {
-    if (workerThread) {
-        if (workerThread->isRunning()) {
-            workerThread->wait();
-        }
-        delete workerThread;
-        workerThread = NULL;
+    if (result) {
+        pmh_free_elements(result);
+        result = NULL;
     }
-    if (cached_elements) {
-        pmh_free_elements(cached_elements);
-        cached_elements = NULL;
+    if (content) {
+        delete [] content;
+        capacity = 0;
+        content = NULL;
     }
 }
 
-void HGMarkdownHighlighter::setStyles(const QVector<HighlightingStyle> &styles)
+void HGMarkdownHighlighter::highlightBlock(const QString &text)
 {
-    this->highlightingStyles = styles;
-}
-
-void HGMarkdownHighlighter::clearFormatting()
-{
-    QTextBlock block = document->firstBlock();
-    while (block.isValid()) {
-        block.layout()->clearAdditionalFormats();
-        block = block.next();
+    int blockNum = currentBlock().blockNumber();
+    if (!parsing && blockHighlights.size() > blockNum) {
+        QVector<HLUnit> &units = blockHighlights[blockNum];
+        for (int i = 0; i < units.size(); ++i) {
+            // TODO: merge two format within the same range
+            const HLUnit& unit = units[i];
+            setFormat(unit.start, unit.length, highlightingStyles[unit.styleIndex].format);
+        }
     }
+    setCurrentBlockState(0);
+    highlightCodeBlock(text);
 }
 
-void HGMarkdownHighlighter::highlightOneRegion(const HighlightingStyle &style,
-                                               unsigned long pos, unsigned long end, bool clearBeforeHighlight)
+void HGMarkdownHighlighter::setStyles(const QVector<HighlightingStyle> &styles)
 {
-    // "The QTextLayout object can only be modified from the
-    // documentChanged implementation of a QAbstractTextDocumentLayout
-    // subclass. Any changes applied from the outside cause undefined
-    // behavior." -- we are breaking this rule here. There might be
-    // a better (more correct) way to do this.
-    int startBlockNum = document->findBlock(pos).blockNumber();
-    int endBlockNum = document->findBlock(end).blockNumber();
-    for (int i = startBlockNum; i <= endBlockNum; ++i)
-    {
-        QTextBlock block = document->findBlockByNumber(i);
-
-        QTextLayout *layout = block.layout();
-        if (clearBeforeHighlight) {
-            layout->clearFormats();
-        }
-        QVector<QTextLayout::FormatRange> list = layout->formats();
-        int blockpos = block.position();
-        QTextLayout::FormatRange r;
-        r.format = style.format;
-
-        if (i == startBlockNum) {
-            r.start = pos - blockpos;
-            r.length = (startBlockNum == endBlockNum)
-                        ? end - pos
-                        : block.length() - r.start;
-        } else if (i == endBlockNum) {
-            r.start = 0;
-            r.length = end - blockpos;
-        } else {
-            r.start = 0;
-            r.length = block.length();
-        }
-
-        list.append(r);
-        layout->setFormats(list);
-    }
+    this->highlightingStyles = styles;
 }
 
-void HGMarkdownHighlighter::highlight()
+void HGMarkdownHighlighter::initBlockHighlightFromResult(int nrBlocks)
 {
-    if (cached_elements == NULL) {
-        return;
+    blockHighlights.resize(nrBlocks);
+    for (int i = 0; i < blockHighlights.size(); ++i) {
+        blockHighlights[i].clear();
     }
 
-    if (highlightingStyles.isEmpty()) {
-        qWarning() << "error: HighlightingStyles is not set";
+    if (!result) {
         return;
     }
 
-    this->clearFormatting();
-
-    // To make sure content is not changed by highlight operations.
-    // May be resource-consuming. Can be removed if no need.
-#ifdef V_HIGHLIGHT_DEBUG
-    QString oriContent = document->toPlainText();
-#endif
-
     for (int i = 0; i < highlightingStyles.size(); i++)
     {
         const HighlightingStyle &style = highlightingStyles[i];
-        pmh_element *elem_cursor = cached_elements[style.type];
+        pmh_element *elem_cursor = result[style.type];
         while (elem_cursor != NULL)
         {
             if (elem_cursor->end <= elem_cursor->pos) {
                 elem_cursor = elem_cursor->next;
                 continue;
             }
-            highlightOneRegion(style, elem_cursor->pos, elem_cursor->end);
+            initBlockHighlihgtOne(elem_cursor->pos, elem_cursor->end, i);
             elem_cursor = elem_cursor->next;
         }
     }
 
-    highlightCodeBlock();
+    pmh_free_elements(result);
+    result = NULL;
+}
 
-    document->markContentsDirty(0, document->characterCount());
+void HGMarkdownHighlighter::initBlockHighlihgtOne(unsigned long pos, unsigned long end, int styleIndex)
+{
+    int startBlockNum = document->findBlock(pos).blockNumber();
+    int endBlockNum = document->findBlock(end).blockNumber();
+    for (int i = startBlockNum; i <= endBlockNum; ++i)
+    {
+        QTextBlock block = document->findBlockByNumber(i);
+        int blockStartPos = block.position();
+        HLUnit unit;
+        if (i == startBlockNum) {
+            unit.start = pos - blockStartPos;
+            unit.length = (startBlockNum == endBlockNum) ?
+                          (end - pos) : (block.length() - unit.start);
+        } else if (i == endBlockNum) {
+            unit.start = 0;
+            unit.length = end - blockStartPos;
+        } else {
+            unit.start = 0;
+            unit.length = block.length();
+        }
+        unit.styleIndex = styleIndex;
 
-#ifdef V_HIGHLIGHT_DEBUG
-    if (oriContent != document->toPlainText()) {
-        qWarning() << "warning: content was changed before and after highlighting";
+        blockHighlights[i].append(unit);
     }
-#endif
-
 }
 
-void HGMarkdownHighlighter::highlightCodeBlock()
+void HGMarkdownHighlighter::highlightCodeBlock(const QString &text)
 {
-    QRegExp codeRegStart("^```");
-    QRegExp codeRegEnd("^```$");
-
-    HighlightingStyle style;
-    int index = 0;
-    for (index = 0; index < highlightingStyles.size(); ++index) {
-        if (highlightingStyles[index].type == pmh_VERBATIM) {
-            style = highlightingStyles[index];
-            break;
+    int nextIndex = 0;
+    int startIndex = 0;
+    if (previousBlockState() != 1) {
+        startIndex = codeBlockStartExp.indexIn(text);
+        if (startIndex >= 0) {
+            nextIndex = startIndex + codeBlockStartExp.matchedLength();
+        } else {
+            nextIndex = -1;
         }
     }
-    if (index == highlightingStyles.size()) {
-        style.type = pmh_VERBATIM;
-        style.format.setForeground(QBrush(Qt::darkYellow));
-    }
-    int pos = 0;
-    while (true) {
-        QTextCursor startCursor = document->find(codeRegStart, pos);
-        if (!startCursor.hasSelection()) {
-            break;
+
+    while (nextIndex >= 0) {
+        int endIndex = codeBlockEndExp.indexIn(text, nextIndex);
+        int codeBlockLength;
+        if (endIndex == -1) {
+            setCurrentBlockState(1);
+            codeBlockLength = text.length() - startIndex;
+        } else {
+            codeBlockLength = endIndex - startIndex + codeBlockEndExp.matchedLength();
         }
-        pos = startCursor.selectionEnd();
-        QTextCursor endCursor = document->find(codeRegEnd, pos);
-        if (!endCursor.hasSelection()) {
-            break;
+        setFormat(startIndex, codeBlockLength, codeBlockFormat);
+        startIndex = codeBlockStartExp.indexIn(text, startIndex + codeBlockLength);
+        if (startIndex >= 0) {
+            nextIndex = startIndex + codeBlockStartExp.matchedLength();
+        } else {
+            nextIndex = -1;
         }
-        pos = endCursor.selectionEnd();
-        highlightOneRegion(style, startCursor.selectionStart(), endCursor.selectionEnd(),
-                           true);
     }
 }
 
 void HGMarkdownHighlighter::parse()
 {
-    if (workerThread->isRunning()) {
-        parsePending = true;
+    if (!parsing.testAndSetRelaxed(0, 1)) {
         return;
     }
 
-    QString content = document->toPlainText();
-    QByteArray ba = content.toUtf8();
-    parsePending = false;
-    workerThread->prepareAndStart((const char *)ba.data());
+    int nrBlocks = document->blockCount();
+    parseInternal();
+
+    if (highlightingStyles.isEmpty()) {
+        qWarning() << "error: HighlightingStyles is not set";
+        return;
+    }
+    initBlockHighlightFromResult(nrBlocks);
+    parsing.store(0);
 }
 
-void HGMarkdownHighlighter::threadFinished()
+void HGMarkdownHighlighter::parseInternal()
 {
-    if (parsePending) {
-        this->parse();
-        return;
+    QString text = document->toPlainText();
+    QByteArray ba = text.toUtf8();
+    const char *data = (const char *)ba.data();
+   int len = ba.size();
+
+    if (result) {
+        pmh_free_elements(result);
+        result = NULL;
     }
 
-    if (cached_elements != NULL) {
-        pmh_free_elements(cached_elements);
+    if (len == 0) {
+        return;
+    } else if (len >= capacity) {
+        resizeBuffer(qMax(2 * capacity, len + 1));
+    } else if (len < (capacity >> 2)) {
+        resizeBuffer(qMax(capacity >> 1, len + 1));
     }
-    cached_elements = workerThread->retriveResult();
-    this->highlight();
+
+    memcpy(content, data, len);
+    content[len] = '\0';
+
+    pmh_markdown_to_elements(content, pmh_EXT_NONE, &result);
 }
 
-void HGMarkdownHighlighter::handleContentsChange(int position, int charsRemoved,
-                                                 int charsAdded)
+void HGMarkdownHighlighter::handleContentChange(int position, int charsRemoved, int charsAdded)
 {
-    if (charsRemoved == 0 && charsAdded == 0)
+    if (charsRemoved == 0 && charsAdded == 0) {
         return;
-
+    }
     timer->stop();
     timer->start();
 }
 
 void HGMarkdownHighlighter::timerTimeout()
 {
-    this->parse();
+    parse();
+    rehighlight();
 }

+ 40 - 36
hgmarkdownhighlighter.h

@@ -11,7 +11,8 @@
 #define HGMARKDOWNHIGHLIGHTER_H
 
 #include <QTextCharFormat>
-#include <QThread>
+#include <QSyntaxHighlighter>
+#include <QAtomicInt>
 
 extern "C" {
 #include "utils/peg-highlight/pmh_parser.h"
@@ -21,62 +22,65 @@ QT_BEGIN_NAMESPACE
 class QTextDocument;
 QT_END_NAMESPACE
 
-class WorkerThread : public QThread
-{
-public:
-    WorkerThread();
-    ~WorkerThread();
-    void prepareAndStart(const char *data);
-    pmh_element** retriveResult();
-
-protected:
-    void run();
-
-private:
-    void resizeBuffer(unsigned int newCap);
-
-    char *content;
-    unsigned int capacity;
-    pmh_element **result;
-    static const unsigned int initCapacity;
-};
-
 struct HighlightingStyle
 {
     pmh_element_type type;
     QTextCharFormat format;
 };
 
-class HGMarkdownHighlighter : public QObject
+// One continuous region for a certain markdown highlight style
+// within a QTextBlock.
+// Pay attention to the change of HighlightingStyles[]
+struct HLUnit
+{
+    // Highlight offset @start and @length with style HighlightingStyles[styleIndex]
+    // within a QTextBlock
+    unsigned long start;
+    unsigned long length;
+    unsigned int styleIndex;
+};
+
+class HGMarkdownHighlighter : public QSyntaxHighlighter
 {
     Q_OBJECT
 
 public:
-    HGMarkdownHighlighter(const QVector<HighlightingStyle> &styles,
-                          QTextDocument *parent = 0, int aWaitInterval = 2000);
+    HGMarkdownHighlighter(const QVector<HighlightingStyle> &styles, int waitInterval,
+                          QTextDocument *parent = 0);
     ~HGMarkdownHighlighter();
     void setStyles(const QVector<HighlightingStyle> &styles);
-    int waitInterval;
+
+protected:
+    void highlightBlock(const QString &text) Q_DECL_OVERRIDE;
 
 private slots:
-    void handleContentsChange(int position, int charsRemoved, int charsAdded);
-    void threadFinished();
+    void handleContentChange(int position, int charsRemoved, int charsAdded);
     void timerTimeout();
 
 private:
-    QTimer *timer;
+    QRegExp codeBlockStartExp;
+    QRegExp codeBlockEndExp;
+    QTextCharFormat codeBlockFormat;
+
     QTextDocument *document;
-    WorkerThread *workerThread;
-    bool parsePending;
-    pmh_element **cached_elements;
     QVector<HighlightingStyle> highlightingStyles;
+    QVector<QVector<HLUnit> > blockHighlights;
+    QAtomicInt parsing;
+    QTimer *timer;
+    int waitInterval;
+
+    char *content;
+    int capacity;
+    pmh_element **result;
+    static const int initCapacity;
+    void resizeBuffer(int newCap);
 
-    void clearFormatting();
-    void highlight();
-    void highlightOneRegion(const HighlightingStyle &style, unsigned long pos,
-                            unsigned long end, bool clearBeforeHighlight = false);
-    void highlightCodeBlock();
+    void highlightCodeBlock(const QString &text);
     void parse();
+    void parseInternal();
+    void initBlockHighlightFromResult(int nrBlocks);
+    void initBlockHighlihgtOne(unsigned long pos, unsigned long end,
+                               int styleIndex);
 };
 
 #endif

+ 5 - 1
vedit.cpp

@@ -2,16 +2,20 @@
 #include "vedit.h"
 #include "vnote.h"
 #include "vconfigmanager.h"
+#include "hgmarkdownhighlighter.h"
 
 extern VConfigManager vconfig;
 
 VEdit::VEdit(VNoteFile *noteFile, QWidget *parent)
-    : QTextEdit(parent), noteFile(noteFile)
+    : QTextEdit(parent), noteFile(noteFile), mdHighlighter(NULL)
 {
     if (noteFile->docType == DocType::Markdown) {
         setPalette(vconfig.getMdEditPalette());
         setFont(vconfig.getMdEditFont());
         setAcceptRichText(false);
+
+        mdHighlighter = new HGMarkdownHighlighter(vconfig.getMdHighlightingStyles(),
+                                                  500, document());
     } else {
         setFont(vconfig.getBaseEditFont());
         setAutoFormatting(QTextEdit::AutoBulletList);

+ 3 - 0
vedit.h

@@ -6,6 +6,8 @@
 #include "vconstants.h"
 #include "vnotefile.h"
 
+class HGMarkdownHighlighter;
+
 class VEdit : public QTextEdit
 {
     Q_OBJECT
@@ -28,6 +30,7 @@ public slots:
 
 private:
     VNoteFile *noteFile;
+    HGMarkdownHighlighter *mdHighlighter;
 };
 
 #endif // VEDIT_H

+ 0 - 4
veditor.cpp

@@ -22,7 +22,6 @@ VEditor::VEditor(const QString &path, const QString &name, bool modifiable,
     noteFile = new VNoteFile(path, name, fileText, docType, modifiable);
 
     isEditMode = false;
-    mdHighlighter = NULL;
 
     setupUI();
 
@@ -45,9 +44,6 @@ void VEditor::setupUI()
     case DocType::Markdown:
         setupMarkdownPreview();
         textBrowser = NULL;
-
-        mdHighlighter = new HGMarkdownHighlighter(vconfig.getMdHighlightingStyles(),
-                                                  textEditor->document(), 500);
         break;
 
     case DocType::Html:

+ 0 - 2
veditor.h

@@ -10,7 +10,6 @@
 class QTextBrowser;
 class VEdit;
 class QWebEngineView;
-class HGMarkdownHighlighter;
 
 class VEditor : public QStackedWidget
 {
@@ -39,7 +38,6 @@ private:
     VEdit *textEditor;
     QWebEngineView *webPreviewer;
     VDocument document;
-    HGMarkdownHighlighter *mdHighlighter;
 };
 
 #endif // VEDITOR_H

+ 5 - 3
vstyleparser.cpp

@@ -115,15 +115,17 @@ void VStyleParser::parseMarkdownStyle(const QString &styleStr)
 
 QVector<HighlightingStyle> VStyleParser::fetchMarkdownStyles(const QFont &baseFont) const
 {
-    QVector<HighlightingStyle> styles(pmh_NUM_LANG_TYPES);
+    QVector<HighlightingStyle> styles;
 
     for (int i = 0; i < pmh_NUM_LANG_TYPES; ++i) {
         pmh_style_attribute *attr = markdownStyles->element_styles[i];
         if (!attr) {
             continue;
         }
-        styles[i].type = attr->lang_element_type;
-        styles[i].format = QTextCharFormatFromAttrs(attr, baseFont);
+        HighlightingStyle style;
+        style.type = attr->lang_element_type;
+        style.format = QTextCharFormatFromAttrs(attr, baseFont);
+        styles.append(style);
     }
     return styles;
 }