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 "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,74 +26,59 @@ 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;
}
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)
HGMarkdownHighlighter::HGMarkdownHighlighter(const QVector<HighlightingStyle> &styles, int waitInterval,
QTextDocument *parent)
: QSyntaxHighlighter(parent), parsing(0),
waitInterval(waitInterval), content(NULL), capacity(0), result(NULL)
{
workerThread = new WorkerThread();
cached_elements = NULL;
waitInterval = aWaitInterval;
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;
}
}
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();
if (result) {
pmh_free_elements(result);
result = NULL;
}
if (content) {
delete [] content;
capacity = 0;
content = 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);
}
delete workerThread;
workerThread = NULL;
}
if (cached_elements) {
pmh_free_elements(cached_elements);
cached_elements = NULL;
}
setCurrentBlockState(0);
highlightCodeBlock(text);
}
void HGMarkdownHighlighter::setStyles(const QVector<HighlightingStyle> &styles)
@ -124,174 +86,148 @@ void HGMarkdownHighlighter::setStyles(const QVector<HighlightingStyle> &styles)
this->highlightingStyles = styles;
}
void HGMarkdownHighlighter::clearFormatting()
void HGMarkdownHighlighter::initBlockHighlightFromResult(int nrBlocks)
{
QTextBlock block = document->firstBlock();
while (block.isValid()) {
block.layout()->clearAdditionalFormats();
block = block.next();
blockHighlights.resize(nrBlocks);
for (int i = 0; i < blockHighlights.size(); ++i) {
blockHighlights[i].clear();
}
}
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);
layout->setFormats(list);
}
}
void HGMarkdownHighlighter::highlight()
{
if (cached_elements == NULL) {
if (!result) {
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++)
{
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();
document->markContentsDirty(0, document->characterCount());
#ifdef V_HIGHLIGHT_DEBUG
if (oriContent != document->toPlainText()) {
qWarning() << "warning: content was changed before and after highlighting";
}
#endif
pmh_free_elements(result);
result = NULL;
}
void HGMarkdownHighlighter::highlightCodeBlock()
void HGMarkdownHighlighter::initBlockHighlihgtOne(unsigned long pos, unsigned long end, int styleIndex)
{
QRegExp codeRegStart("^```");
QRegExp codeRegEnd("^```$");
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;
HighlightingStyle style;
int index = 0;
for (index = 0; index < highlightingStyles.size(); ++index) {
if (highlightingStyles[index].type == pmh_VERBATIM) {
style = highlightingStyles[index];
break;
blockHighlights[i].append(unit);
}
}
void HGMarkdownHighlighter::highlightCodeBlock(const QString &text)
{
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();
}

View File

@ -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;
QTextDocument *document;
WorkerThread *workerThread;
bool parsePending;
pmh_element **cached_elements;
QVector<HighlightingStyle> highlightingStyles;
QRegExp codeBlockStartExp;
QRegExp codeBlockEndExp;
QTextCharFormat codeBlockFormat;
void clearFormatting();
void highlight();
void highlightOneRegion(const HighlightingStyle &style, unsigned long pos,
unsigned long end, bool clearBeforeHighlight = false);
void highlightCodeBlock();
QTextDocument *document;
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 highlightCodeBlock(const QString &text);
void parse();
void parseInternal();
void initBlockHighlightFromResult(int nrBlocks);
void initBlockHighlihgtOne(unsigned long pos, unsigned long end,
int styleIndex);
};
#endif

View File

@ -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);

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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;
}