579 lines
13 KiB
C++
579 lines
13 KiB
C++
#include "QtHighlighter.h"
|
|
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QTextBlock>
|
|
#include <QTextCursor>
|
|
#include <QTextDocument>
|
|
|
|
#include "ColorScheme.h"
|
|
#include "FileSystem.h"
|
|
#include "ResourcePaths.h"
|
|
#include "TextAccess.h"
|
|
#include "logging.h"
|
|
#include "tracing.h"
|
|
#include "utility.h"
|
|
|
|
std::map<std::wstring, std::vector<QtHighlighter::HighlightingRule>> QtHighlighter::s_highlightingRules;
|
|
std::map<QtHighlighter::HighlightType, QTextCharFormat> QtHighlighter::s_charFormats;
|
|
|
|
std::string QtHighlighter::highlightTypeToString(QtHighlighter::HighlightType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case HighlightType::COMMENT:
|
|
return "comment";
|
|
case HighlightType::DIRECTIVE:
|
|
return "directive";
|
|
case HighlightType::FUNCTION:
|
|
return "function";
|
|
case HighlightType::KEYWORD:
|
|
return "keyword";
|
|
case HighlightType::NUMBER:
|
|
return "number";
|
|
case HighlightType::QUOTATION:
|
|
return "quotation";
|
|
case HighlightType::TEXT:
|
|
return "text";
|
|
case HighlightType::TYPE:
|
|
return "type";
|
|
}
|
|
return "text";
|
|
}
|
|
|
|
QtHighlighter::HighlightType QtHighlighter::highlightTypeFromString(const std::string typeStr)
|
|
{
|
|
const std::array<HighlightType, 8> types = {
|
|
HighlightType::COMMENT,
|
|
HighlightType::DIRECTIVE,
|
|
HighlightType::FUNCTION,
|
|
HighlightType::KEYWORD,
|
|
HighlightType::NUMBER,
|
|
HighlightType::QUOTATION,
|
|
HighlightType::TEXT,
|
|
HighlightType::TYPE};
|
|
|
|
for (HighlightType type: types)
|
|
{
|
|
if (typeStr == highlightTypeToString(type))
|
|
{
|
|
return type;
|
|
}
|
|
}
|
|
|
|
return HighlightType::TEXT;
|
|
}
|
|
|
|
void QtHighlighter::loadHighlightingRules()
|
|
{
|
|
ColorScheme* scheme = ColorScheme::getInstance().get();
|
|
|
|
const std::array<HighlightType, 8> types = {
|
|
HighlightType::COMMENT,
|
|
HighlightType::DIRECTIVE,
|
|
HighlightType::FUNCTION,
|
|
HighlightType::KEYWORD,
|
|
HighlightType::NUMBER,
|
|
HighlightType::QUOTATION,
|
|
HighlightType::TEXT,
|
|
HighlightType::TYPE};
|
|
|
|
s_charFormats.clear();
|
|
for (HighlightType type: types)
|
|
{
|
|
QTextCharFormat format;
|
|
format.setForeground(QColor(scheme->getSyntaxColor(highlightTypeToString(type)).c_str()));
|
|
s_charFormats.emplace(type, format);
|
|
}
|
|
|
|
for (const FilePath path: FileSystem::getFilePathsFromDirectory(
|
|
ResourcePaths::getSyntaxHighlightingRulesPath(), {L".rules"}))
|
|
{
|
|
std::wstring language = path.withoutExtension().fileName();
|
|
|
|
std::shared_ptr<TextAccess> textAccess = TextAccess::createFromFile(path);
|
|
|
|
QJsonParseError error;
|
|
QJsonDocument doc = QJsonDocument::fromJson(
|
|
QString::fromStdString(textAccess->getText()).toUtf8(), &error);
|
|
if (doc.isNull() || !doc.isArray())
|
|
{
|
|
LOG_ERROR_STREAM(
|
|
<< "Highlinghting rules in \"" << path.str()
|
|
<< "\" couldn't be parsed as JSON: "
|
|
"offset "
|
|
<< error.offset << " - " << error.errorString().toStdString());
|
|
continue;
|
|
}
|
|
|
|
std::vector<HighlightingRule> rules;
|
|
|
|
for (QJsonValueRef value: doc.array())
|
|
{
|
|
if (!value.isObject())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
QJsonObject ruleObj = value.toObject();
|
|
|
|
HighlightType type = highlightTypeFromString(
|
|
ruleObj.value(QStringLiteral("type")).toString().toStdString());
|
|
|
|
bool priority = ruleObj.value(QStringLiteral("priority")).toBool();
|
|
|
|
QJsonArray patterns = ruleObj.value(QStringLiteral("patterns")).toArray();
|
|
for (QJsonValueRef pattern: patterns)
|
|
{
|
|
if (pattern.isString())
|
|
{
|
|
rules.push_back(HighlightingRule(type, QRegExp(pattern.toString()), priority));
|
|
}
|
|
}
|
|
|
|
QJsonObject range = ruleObj.value(QStringLiteral("range")).toObject();
|
|
if (!range.empty())
|
|
{
|
|
rules.push_back(HighlightingRule(
|
|
type, QRegExp(range.value("start").toString()), priority, true));
|
|
rules.push_back(
|
|
HighlightingRule(type, QRegExp(range.value("end").toString()), priority, true));
|
|
}
|
|
}
|
|
|
|
s_highlightingRules.emplace(language, rules);
|
|
}
|
|
}
|
|
|
|
void QtHighlighter::clearHighlightingRules()
|
|
{
|
|
s_highlightingRules.clear();
|
|
}
|
|
|
|
QtHighlighter::QtHighlighter(QTextDocument* document, const std::wstring& language)
|
|
: m_document(document)
|
|
{
|
|
if (!s_highlightingRules.size())
|
|
{
|
|
loadHighlightingRules();
|
|
}
|
|
|
|
const auto it = s_highlightingRules.find(language);
|
|
if (it != s_highlightingRules.end())
|
|
{
|
|
m_highlightingRules = it->second;
|
|
}
|
|
}
|
|
|
|
void QtHighlighter::highlightDocument()
|
|
{
|
|
TRACE();
|
|
|
|
QTextDocument* doc = document();
|
|
|
|
size_t docStart = 0;
|
|
size_t docEnd = 0;
|
|
for (int i = 0; i < doc->blockCount(); i++)
|
|
{
|
|
docEnd += doc->findBlockByLineNumber(i).length();
|
|
}
|
|
|
|
if (docEnd > 0)
|
|
{
|
|
docEnd -= 1;
|
|
}
|
|
|
|
applyFormat(docStart, docEnd, s_charFormats[HighlightType::TEXT]);
|
|
|
|
m_highlightedLines.clear();
|
|
m_highlightedLines.resize(document()->blockCount(), false);
|
|
|
|
if (m_highlightingRules.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::vector<HighlightingRule> singleLineRules;
|
|
for (const HighlightingRule& rule: m_highlightingRules)
|
|
{
|
|
if (rule.priority && !rule.multiLine)
|
|
{
|
|
singleLineRules.emplace_back(rule);
|
|
}
|
|
}
|
|
createRanges(doc, singleLineRules);
|
|
}
|
|
|
|
void QtHighlighter::highlightRange(int startLine, int endLine)
|
|
{
|
|
if (startLine < 0 || endLine < 0 || startLine > endLine ||
|
|
endLine > int(m_highlightedLines.size()))
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool hasUnhighlightedLines = false;
|
|
for (int i = startLine; i <= endLine; i++)
|
|
{
|
|
if (!m_highlightedLines[i])
|
|
{
|
|
hasUnhighlightedLines = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!hasUnhighlightedLines)
|
|
{
|
|
return;
|
|
}
|
|
|
|
QTextDocument* doc = document();
|
|
QTextBlock start = doc->findBlockByLineNumber(startLine);
|
|
QTextBlock end = doc->findBlockByLineNumber(endLine + 1);
|
|
|
|
const HighlightingRule* singleLineCommentRule = nullptr;
|
|
const HighlightingRule* quotationRule = nullptr;
|
|
|
|
for (const HighlightingRule& rule: m_highlightingRules)
|
|
{
|
|
if (rule.type == HighlightType::COMMENT && !rule.multiLine)
|
|
{
|
|
singleLineCommentRule = &rule;
|
|
}
|
|
else if (rule.type == HighlightType::QUOTATION)
|
|
{
|
|
quotationRule = &rule;
|
|
}
|
|
}
|
|
|
|
int index = startLine;
|
|
for (QTextBlock it = start; it != end; it = it.next())
|
|
{
|
|
if (!m_highlightedLines[index])
|
|
{
|
|
applyFormat(
|
|
it.position(), it.position() + it.length() - 1, s_charFormats[HighlightType::TEXT]);
|
|
|
|
for (const HighlightingRule& rule: m_highlightingRules)
|
|
{
|
|
if (rule.multiLine)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (rule.priority)
|
|
{
|
|
formatBlockIfInRange(it, rule.type, &m_singleLineRanges);
|
|
}
|
|
else
|
|
{
|
|
formatBlockForRule(it, rule, &m_singleLineRanges);
|
|
}
|
|
}
|
|
|
|
if (m_multiLineRanges.size())
|
|
{
|
|
formatBlockIfInRange(it, &m_multiLineRanges);
|
|
}
|
|
}
|
|
index++;
|
|
}
|
|
|
|
for (int i = startLine; i <= endLine; i++)
|
|
{
|
|
m_highlightedLines[i] = true;
|
|
}
|
|
}
|
|
|
|
void QtHighlighter::rehighlightLines(const std::vector<int>& lines)
|
|
{
|
|
for (int line: lines)
|
|
{
|
|
if (line >= 0 && line < int(m_highlightedLines.size()))
|
|
{
|
|
m_highlightedLines[line] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void QtHighlighter::applyFormat(int startPosition, int endPosition, const QTextCharFormat& format)
|
|
{
|
|
QTextCursor cursor(document());
|
|
cursor.setPosition(startPosition);
|
|
cursor.setPosition(endPosition, QTextCursor::KeepAnchor);
|
|
cursor.setCharFormat(format);
|
|
}
|
|
|
|
QTextCharFormat QtHighlighter::getFormat(int startPosition, int endPosition) const
|
|
{
|
|
QTextCursor cursor(document());
|
|
cursor.setPosition(endPosition);
|
|
return cursor.charFormat();
|
|
}
|
|
|
|
void QtHighlighter::createRanges(QTextDocument* doc, const std::vector<HighlightingRule>& singleLineRules)
|
|
{
|
|
m_singleLineRanges.clear();
|
|
m_multiLineRanges.clear();
|
|
|
|
for (QTextBlock it = doc->begin(); it != doc->end(); it = it.next())
|
|
{
|
|
for (const HighlightingRule& rule: singleLineRules)
|
|
{
|
|
utility::append(m_singleLineRanges, getRangesForRule(it, rule));
|
|
}
|
|
}
|
|
|
|
// remove ranges starting inside others
|
|
{
|
|
std::map<std::pair<int, int>, size_t> sortedRangesToIndex;
|
|
for (size_t i = 0; i < m_singleLineRanges.size(); i++)
|
|
{
|
|
const std::tuple<HighlightType, int, int>& range = m_singleLineRanges[i];
|
|
sortedRangesToIndex.emplace(std::make_pair(std::get<1>(range), std::get<2>(range)), i);
|
|
}
|
|
|
|
std::set<size_t> indicesToErase;
|
|
const std::pair<int, int>* topRange = nullptr;
|
|
for (const auto& p: sortedRangesToIndex)
|
|
{
|
|
if (topRange && p.first.first <= topRange->second)
|
|
{
|
|
indicesToErase.insert(p.second);
|
|
}
|
|
else
|
|
{
|
|
topRange = &p.first;
|
|
}
|
|
}
|
|
|
|
for (std::set<size_t>::const_reverse_iterator it = indicesToErase.rbegin();
|
|
it != indicesToErase.rend();
|
|
it++)
|
|
{
|
|
m_singleLineRanges.erase(m_singleLineRanges.begin() + *it);
|
|
}
|
|
}
|
|
|
|
m_multiLineRanges = createMultiLineRanges(doc, &m_singleLineRanges);
|
|
}
|
|
|
|
std::vector<std::tuple<QtHighlighter::HighlightType, int, int>> QtHighlighter::createMultiLineRanges(
|
|
QTextDocument* doc, std::vector<std::tuple<HighlightType, int, int>>* ranges)
|
|
{
|
|
std::vector<std::tuple<HighlightType, int, int>> multiLineRanges;
|
|
|
|
const HighlightingRule* startRule = nullptr;
|
|
|
|
for (const HighlightingRule& rule: m_highlightingRules)
|
|
{
|
|
if (rule.priority && rule.multiLine)
|
|
{
|
|
if (!startRule)
|
|
{
|
|
startRule = &rule;
|
|
}
|
|
else if (rule.type == startRule->type)
|
|
{
|
|
utility::append(
|
|
multiLineRanges, createMultiLineRangesForRules(doc, ranges, startRule, &rule));
|
|
startRule = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
return multiLineRanges;
|
|
}
|
|
|
|
std::vector<std::tuple<QtHighlighter::HighlightType, int, int>> QtHighlighter::createMultiLineRangesForRules(
|
|
QTextDocument* doc,
|
|
std::vector<std::tuple<HighlightType, int, int>>* ranges,
|
|
const HighlightingRule* startRule,
|
|
const HighlightingRule* endRule)
|
|
{
|
|
std::vector<std::tuple<HighlightType, int, int>> multiLineRanges;
|
|
|
|
QTextCursor cursorStart(doc);
|
|
QTextCursor cursorEnd(doc);
|
|
|
|
while (true)
|
|
{
|
|
while (true)
|
|
{
|
|
cursorStart = document()->find(startRule->pattern, cursorStart);
|
|
if (cursorStart.isNull())
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (!isInRange(cursorStart.selectionEnd() - 1, *ranges))
|
|
{
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
cursorStart.setPosition(cursorStart.selectionEnd() + 1);
|
|
}
|
|
}
|
|
|
|
if (cursorStart.isNull())
|
|
{
|
|
break;
|
|
}
|
|
|
|
cursorEnd = document()->find(endRule->pattern, cursorStart);
|
|
if (cursorEnd.isNull())
|
|
{
|
|
break;
|
|
}
|
|
|
|
multiLineRanges.emplace_back(
|
|
std::make_tuple(startRule->type, cursorStart.selectionStart(), cursorEnd.position()));
|
|
|
|
cursorStart = cursorEnd;
|
|
}
|
|
|
|
return multiLineRanges;
|
|
}
|
|
|
|
QtHighlighter::HighlightingRule::HighlightingRule() {}
|
|
|
|
QtHighlighter::HighlightingRule::HighlightingRule(
|
|
HighlightType type, const QRegExp& regExp, bool priority, bool multiLine)
|
|
: type(type), pattern(regExp), priority(priority), multiLine(multiLine)
|
|
{
|
|
}
|
|
|
|
bool QtHighlighter::isInRange(int pos, const std::vector<std::tuple<HighlightType, int, int>>& ranges) const
|
|
{
|
|
for (const std::tuple<HighlightType, int, int>& range: ranges)
|
|
{
|
|
if (pos >= std::get<1>(range) && pos <= std::get<2>(range))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
std::vector<std::tuple<QtHighlighter::HighlightType, int, int>> QtHighlighter::getRangesForRule(
|
|
const QTextBlock& block, const HighlightingRule& rule) const
|
|
{
|
|
const int pos = block.position();
|
|
const QString text = block.text();
|
|
QRegExp expression(rule.pattern);
|
|
int index = expression.indexIn(text);
|
|
|
|
std::vector<std::tuple<HighlightType, int, int>> ranges;
|
|
|
|
while (index >= 0)
|
|
{
|
|
const int length = expression.matchedLength();
|
|
if (expression.capturedTexts().size() > 1)
|
|
{
|
|
const QString cap = expression.capturedTexts()[1];
|
|
const int start = text.indexOf(cap, index);
|
|
ranges.push_back(std::make_tuple(rule.type, pos + start, pos + start + cap.length()));
|
|
}
|
|
else
|
|
{
|
|
ranges.push_back(std::make_tuple(rule.type, pos + index, pos + index + length));
|
|
}
|
|
index = expression.indexIn(block.text(), index + length);
|
|
}
|
|
|
|
return ranges;
|
|
}
|
|
|
|
void QtHighlighter::formatBlockForRule(
|
|
const QTextBlock& block,
|
|
const HighlightingRule& rule,
|
|
std::vector<std::tuple<HighlightType, int, int>>* ranges)
|
|
{
|
|
if (s_charFormats.find(rule.type) == s_charFormats.end())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const QTextCharFormat& format = s_charFormats.find(rule.type)->second;
|
|
|
|
QRegExp expression(rule.pattern);
|
|
int pos = block.position();
|
|
int index = expression.indexIn(block.text());
|
|
|
|
while (index >= 0)
|
|
{
|
|
int length = expression.matchedLength();
|
|
|
|
if (!isInRange(pos + index, *ranges))
|
|
{
|
|
applyFormat(pos + index, pos + index + length, format);
|
|
}
|
|
|
|
index = expression.indexIn(block.text(), index + length);
|
|
}
|
|
}
|
|
|
|
void QtHighlighter::formatBlockIfInRange(
|
|
const QTextBlock& block,
|
|
HighlightType type,
|
|
std::vector<std::tuple<HighlightType, int, int>>* ranges)
|
|
{
|
|
int startPos = block.position();
|
|
int endPos = startPos + block.length() - 1;
|
|
|
|
if (s_charFormats.find(type) == s_charFormats.end())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const QTextCharFormat& format = s_charFormats.find(type)->second;
|
|
|
|
for (auto range: *ranges)
|
|
{
|
|
if (type == std::get<0>(range))
|
|
{
|
|
int start = std::max(std::get<1>(range), startPos);
|
|
int end = std::min(std::get<2>(range), endPos);
|
|
|
|
if (start <= end)
|
|
{
|
|
applyFormat(start, end, format);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void QtHighlighter::formatBlockIfInRange(
|
|
const QTextBlock& block, std::vector<std::tuple<HighlightType, int, int>>* ranges)
|
|
{
|
|
int startPos = block.position();
|
|
int endPos = startPos + block.length() - 1;
|
|
|
|
for (auto range: *ranges)
|
|
{
|
|
HighlightType type = std::get<0>(range);
|
|
if (s_charFormats.find(type) == s_charFormats.end())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const QTextCharFormat& format = s_charFormats.find(type)->second;
|
|
|
|
int start = std::max(std::get<1>(range), startPos);
|
|
int end = std::min(std::get<2>(range), endPos);
|
|
|
|
if (start <= end)
|
|
{
|
|
applyFormat(start, end, format);
|
|
}
|
|
}
|
|
}
|
|
|
|
QTextDocument* QtHighlighter::document() const
|
|
{
|
|
return m_document;
|
|
}
|