change HGMarkdownHighlighter to use QSyntaxHighlighter

Signed-off-by: Le Tan <tamlokveer@gmail.com>
This commit is contained in:
Le Tan 2016-10-19 21:25:32 +08:00
parent 4a1c5e0a91
commit 29609f0b65
7 changed files with 192 additions and 249 deletions

View File

@ -11,32 +11,9 @@
#include <QtDebug> #include <QtDebug>
#include "hgmarkdownhighlighter.h" #include "hgmarkdownhighlighter.h"
#ifndef QT_NO_DEBUG const int HGMarkdownHighlighter::initCapacity = 1024;
#define V_HIGHLIGHT_DEBUG
#endif
const unsigned int WorkerThread::initCapacity = 1024; void HGMarkdownHighlighter::resizeBuffer(int newCap)
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)
{ {
if (newCap == capacity) { if (newCap == capacity) {
return; return;
@ -49,74 +26,59 @@ void WorkerThread::resizeBuffer(unsigned int newCap)
content = new char [capacity]; 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;
}
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 // Will be freeed by parent automatically
HGMarkdownHighlighter::HGMarkdownHighlighter(const QVector<HighlightingStyle> &styles, HGMarkdownHighlighter::HGMarkdownHighlighter(const QVector<HighlightingStyle> &styles, int waitInterval,
QTextDocument *parent, QTextDocument *parent)
int aWaitInterval) : QObject(parent) : QSyntaxHighlighter(parent), parsing(0),
waitInterval(waitInterval), content(NULL), capacity(0), result(NULL)
{ {
workerThread = new WorkerThread(); codeBlockStartExp = QRegExp("^```");
cached_elements = NULL; codeBlockEndExp = QRegExp("^```$");
waitInterval = aWaitInterval; codeBlockFormat.setForeground(QBrush(Qt::darkYellow));
for (int index = 0; index < styles.size(); ++index) {
if (styles[index].type == pmh_VERBATIM) {
codeBlockFormat = styles[index].format;
break;
}
}
resizeBuffer(initCapacity);
setStyles(styles); setStyles(styles);
document = parent;
timer = new QTimer(this); timer = new QTimer(this);
timer->setSingleShot(true); timer->setSingleShot(true);
timer->setInterval(aWaitInterval); timer->setInterval(this->waitInterval);
connect(timer, SIGNAL(timeout()), this, SLOT(timerTimeout())); connect(timer, &QTimer::timeout, this, &HGMarkdownHighlighter::timerTimeout);
document = parent; connect(document, &QTextDocument::contentsChange,
connect(document, SIGNAL(contentsChange(int,int,int)), this, &HGMarkdownHighlighter::handleContentChange);
this, SLOT(handleContentsChange(int,int,int)));
connect(workerThread, SIGNAL(finished()), this, SLOT(threadFinished()));
this->parse();
} }
HGMarkdownHighlighter::~HGMarkdownHighlighter() HGMarkdownHighlighter::~HGMarkdownHighlighter()
{ {
if (workerThread) { if (result) {
if (workerThread->isRunning()) { pmh_free_elements(result);
workerThread->wait(); result = NULL;
} }
delete workerThread; if (content) {
workerThread = NULL; delete [] content;
capacity = 0;
content = NULL;
} }
if (cached_elements) { }
pmh_free_elements(cached_elements);
cached_elements = NULL; void HGMarkdownHighlighter::highlightBlock(const QString &text)
{
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::setStyles(const QVector<HighlightingStyle> &styles) void HGMarkdownHighlighter::setStyles(const QVector<HighlightingStyle> &styles)
@ -124,174 +86,148 @@ void HGMarkdownHighlighter::setStyles(const QVector<HighlightingStyle> &styles)
this->highlightingStyles = styles; this->highlightingStyles = styles;
} }
void HGMarkdownHighlighter::clearFormatting() void HGMarkdownHighlighter::initBlockHighlightFromResult(int nrBlocks)
{ {
QTextBlock block = document->firstBlock(); blockHighlights.resize(nrBlocks);
while (block.isValid()) { for (int i = 0; i < blockHighlights.size(); ++i) {
block.layout()->clearAdditionalFormats(); blockHighlights[i].clear();
block = block.next();
}
}
void HGMarkdownHighlighter::highlightOneRegion(const HighlightingStyle &style,
unsigned long pos, unsigned long end, bool clearBeforeHighlight)
{
// "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); if (!result) {
layout->setFormats(list);
}
}
void HGMarkdownHighlighter::highlight()
{
if (cached_elements == NULL) {
return; return;
} }
if (highlightingStyles.isEmpty()) {
qWarning() << "error: HighlightingStyles is not set";
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++) for (int i = 0; i < highlightingStyles.size(); i++)
{ {
const HighlightingStyle &style = highlightingStyles[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) while (elem_cursor != NULL)
{ {
if (elem_cursor->end <= elem_cursor->pos) { if (elem_cursor->end <= elem_cursor->pos) {
elem_cursor = elem_cursor->next; elem_cursor = elem_cursor->next;
continue; continue;
} }
highlightOneRegion(style, elem_cursor->pos, elem_cursor->end); initBlockHighlihgtOne(elem_cursor->pos, elem_cursor->end, i);
elem_cursor = elem_cursor->next; elem_cursor = elem_cursor->next;
} }
} }
highlightCodeBlock(); pmh_free_elements(result);
result = NULL;
document->markContentsDirty(0, document->characterCount());
#ifdef V_HIGHLIGHT_DEBUG
if (oriContent != document->toPlainText()) {
qWarning() << "warning: content was changed before and after highlighting";
}
#endif
} }
void HGMarkdownHighlighter::highlightCodeBlock() void HGMarkdownHighlighter::initBlockHighlihgtOne(unsigned long pos, unsigned long end, int styleIndex)
{ {
QRegExp codeRegStart("^```"); int startBlockNum = document->findBlock(pos).blockNumber();
QRegExp codeRegEnd("^```$"); 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;
HighlightingStyle style; blockHighlights[i].append(unit);
int index = 0; }
for (index = 0; index < highlightingStyles.size(); ++index) { }
if (highlightingStyles[index].type == pmh_VERBATIM) {
style = highlightingStyles[index]; void HGMarkdownHighlighter::highlightCodeBlock(const QString &text)
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; while (nextIndex >= 0) {
style.format.setForeground(QBrush(Qt::darkYellow)); int endIndex = codeBlockEndExp.indexIn(text, nextIndex);
int codeBlockLength;
if (endIndex == -1) {
setCurrentBlockState(1);
codeBlockLength = text.length() - startIndex;
} else {
codeBlockLength = endIndex - startIndex + codeBlockEndExp.matchedLength();
} }
int pos = 0; setFormat(startIndex, codeBlockLength, codeBlockFormat);
while (true) { startIndex = codeBlockStartExp.indexIn(text, startIndex + codeBlockLength);
QTextCursor startCursor = document->find(codeRegStart, pos); if (startIndex >= 0) {
if (!startCursor.hasSelection()) { nextIndex = startIndex + codeBlockStartExp.matchedLength();
break; } else {
nextIndex = -1;
} }
pos = startCursor.selectionEnd();
QTextCursor endCursor = document->find(codeRegEnd, pos);
if (!endCursor.hasSelection()) {
break;
}
pos = endCursor.selectionEnd();
highlightOneRegion(style, startCursor.selectionStart(), endCursor.selectionEnd(),
true);
} }
} }
void HGMarkdownHighlighter::parse() void HGMarkdownHighlighter::parse()
{ {
if (workerThread->isRunning()) { if (!parsing.testAndSetRelaxed(0, 1)) {
parsePending = true;
return; return;
} }
QString content = document->toPlainText(); int nrBlocks = document->blockCount();
QByteArray ba = content.toUtf8(); parseInternal();
parsePending = false;
workerThread->prepareAndStart((const char *)ba.data()); if (highlightingStyles.isEmpty()) {
qWarning() << "error: HighlightingStyles is not set";
return;
}
initBlockHighlightFromResult(nrBlocks);
parsing.store(0);
} }
void HGMarkdownHighlighter::threadFinished() void HGMarkdownHighlighter::parseInternal()
{ {
if (parsePending) { QString text = document->toPlainText();
this->parse(); QByteArray ba = text.toUtf8();
return; const char *data = (const char *)ba.data();
int len = ba.size();
if (result) {
pmh_free_elements(result);
result = NULL;
} }
if (cached_elements != NULL) { if (len == 0) {
pmh_free_elements(cached_elements); 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, void HGMarkdownHighlighter::handleContentChange(int position, int charsRemoved, int charsAdded)
int charsAdded)
{ {
if (charsRemoved == 0 && charsAdded == 0) if (charsRemoved == 0 && charsAdded == 0) {
return; return;
}
timer->stop(); timer->stop();
timer->start(); timer->start();
} }
void HGMarkdownHighlighter::timerTimeout() void HGMarkdownHighlighter::timerTimeout()
{ {
this->parse(); parse();
rehighlight();
} }

View File

@ -11,7 +11,8 @@
#define HGMARKDOWNHIGHLIGHTER_H #define HGMARKDOWNHIGHLIGHTER_H
#include <QTextCharFormat> #include <QTextCharFormat>
#include <QThread> #include <QSyntaxHighlighter>
#include <QAtomicInt>
extern "C" { extern "C" {
#include "utils/peg-highlight/pmh_parser.h" #include "utils/peg-highlight/pmh_parser.h"
@ -21,62 +22,65 @@ QT_BEGIN_NAMESPACE
class QTextDocument; class QTextDocument;
QT_END_NAMESPACE 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 struct HighlightingStyle
{ {
pmh_element_type type; pmh_element_type type;
QTextCharFormat format; 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 Q_OBJECT
public: public:
HGMarkdownHighlighter(const QVector<HighlightingStyle> &styles, HGMarkdownHighlighter(const QVector<HighlightingStyle> &styles, int waitInterval,
QTextDocument *parent = 0, int aWaitInterval = 2000); QTextDocument *parent = 0);
~HGMarkdownHighlighter(); ~HGMarkdownHighlighter();
void setStyles(const QVector<HighlightingStyle> &styles); void setStyles(const QVector<HighlightingStyle> &styles);
int waitInterval;
protected:
void highlightBlock(const QString &text) Q_DECL_OVERRIDE;
private slots: private slots:
void handleContentsChange(int position, int charsRemoved, int charsAdded); void handleContentChange(int position, int charsRemoved, int charsAdded);
void threadFinished();
void timerTimeout(); void timerTimeout();
private: private:
QTimer *timer; QRegExp codeBlockStartExp;
QTextDocument *document; QRegExp codeBlockEndExp;
WorkerThread *workerThread; QTextCharFormat codeBlockFormat;
bool parsePending;
pmh_element **cached_elements;
QVector<HighlightingStyle> highlightingStyles;
void clearFormatting(); QTextDocument *document;
void highlight(); QVector<HighlightingStyle> highlightingStyles;
void highlightOneRegion(const HighlightingStyle &style, unsigned long pos, QVector<QVector<HLUnit> > blockHighlights;
unsigned long end, bool clearBeforeHighlight = false); QAtomicInt parsing;
void highlightCodeBlock(); QTimer *timer;
int waitInterval;
char *content;
int capacity;
pmh_element **result;
static const int initCapacity;
void resizeBuffer(int newCap);
void highlightCodeBlock(const QString &text);
void parse(); void parse();
void parseInternal();
void initBlockHighlightFromResult(int nrBlocks);
void initBlockHighlihgtOne(unsigned long pos, unsigned long end,
int styleIndex);
}; };
#endif #endif

View File

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

View File

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

View File

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

View File

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

View File

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