gpt4all/gpt4all-chat/chatviewtextprocessor.cpp
AT 37dbd56153
Latest rc5 fixes (#2492)
* Adjust the size of the new conversation tray to enlarge a bit.

* Add themeable code syntax highlighting.

* Change the default size to a larger context chunk for localdocs.

Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-30 19:15:01 -04:00

1130 lines
38 KiB
C++

#include "chatviewtextprocessor.h"
#include <QBrush>
#include <QChar>
#include <QClipboard>
#include <QFont>
#include <QFontMetricsF>
#include <QGuiApplication>
#include <QList>
#include <QPainter>
#include <QQuickTextDocument>
#include <QRegularExpression>
#include <QStringList>
#include <QTextBlock>
#include <QTextCharFormat>
#include <QTextCursor>
#include <QTextDocument>
#include <QTextDocumentFragment>
#include <QTextFrame>
#include <QTextFrameFormat>
#include <QTextTableCell>
#include <QVariant>
#include <Qt>
#include <QtGlobal>
#include <algorithm>
enum Language {
None,
Python,
Cpp,
Bash,
TypeScript,
Java,
Go,
Json,
Csharp,
Latex,
Html,
Php,
Markdown
};
static Language stringToLanguage(const QString &language)
{
if (language == "python")
return Python;
if (language == "cpp")
return Cpp;
if (language == "c++")
return Cpp;
if (language == "csharp")
return Csharp;
if (language == "c#")
return Csharp;
if (language == "c")
return Cpp;
if (language == "bash")
return Bash;
if (language == "javascript")
return TypeScript;
if (language == "typescript")
return TypeScript;
if (language == "java")
return Java;
if (language == "go")
return Go;
if (language == "golang")
return Go;
if (language == "json")
return Json;
if (language == "latex")
return Latex;
if (language == "html")
return Html;
if (language == "php")
return Php;
return None;
}
enum Code {
Default,
Keyword,
Function,
FunctionCall,
Comment,
String,
Number,
Header,
Preprocessor,
Type,
Arrow,
Command,
Variable,
Key,
Value,
Parameter,
AttributeName,
AttributeValue,
SpecialCharacter,
DocType
};
struct HighlightingRule {
QRegularExpression pattern;
Code format;
};
static QColor formatToColor(Code c, const CodeColors &colors)
{
switch (c) {
case Default: return colors.defaultColor;
case Keyword: return colors.keywordColor;
case Function: return colors.functionColor;
case FunctionCall: return colors.functionCallColor;
case Comment: return colors.commentColor;
case String: return colors.stringColor;
case Number: return colors.numberColor;
case Header: return colors.headerColor;
case Preprocessor: return colors.preprocessorColor;
case Type: return colors.typeColor;
case Arrow: return colors.arrowColor;
case Command: return colors.commandColor;
case Variable: return colors.variableColor;
case Key: return colors.keyColor;
case Value: return colors.valueColor;
case Parameter: return colors.parameterColor;
case AttributeName: return colors.attributeNameColor;
case AttributeValue: return colors.attributeValueColor;
case SpecialCharacter: return colors.specialCharacterColor;
case DocType: return colors.doctypeColor;
default: Q_UNREACHABLE();
}
return QColor();
}
static QVector<HighlightingRule> pythonHighlightingRules()
{
static QVector<HighlightingRule> highlightingRules;
if (highlightingRules.isEmpty()) {
HighlightingRule rule;
rule.pattern = QRegularExpression(".*");
rule.format = Default;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\b(\\w+)\\s*(?=\\()");
rule.format = FunctionCall;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\bdef\\s+(\\w+)\\b");
rule.format = Function;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\b[0-9]*\\.?[0-9]+\\b");
rule.format = Number;
highlightingRules.append(rule);
QStringList keywordPatterns = {
"\\bdef\\b", "\\bclass\\b", "\\bif\\b", "\\belse\\b", "\\belif\\b",
"\\bwhile\\b", "\\bfor\\b", "\\breturn\\b", "\\bprint\\b", "\\bimport\\b",
"\\bfrom\\b", "\\bas\\b", "\\btry\\b", "\\bexcept\\b", "\\braise\\b",
"\\bwith\\b", "\\bfinally\\b", "\\bcontinue\\b", "\\bbreak\\b", "\\bpass\\b"
};
for (const QString &pattern : keywordPatterns) {
rule.pattern = QRegularExpression(pattern);
rule.format = Keyword;
highlightingRules.append(rule);
}
rule.pattern = QRegularExpression("\".*?\"");
rule.format = String;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\'.*?\'");
rule.format = String;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("#[^\n]*");
rule.format = Comment;
highlightingRules.append(rule);
}
return highlightingRules;
}
static QVector<HighlightingRule> csharpHighlightingRules()
{
static QVector<HighlightingRule> highlightingRules;
if (highlightingRules.isEmpty()) {
HighlightingRule rule;
rule.pattern = QRegularExpression(".*");
rule.format = Default;
highlightingRules.append(rule);
// Function call highlighting
rule.pattern = QRegularExpression("\\b(\\w+)\\s*(?=\\()");
rule.format = FunctionCall;
highlightingRules.append(rule);
// Function definition highlighting
rule.pattern = QRegularExpression("\\bvoid|int|double|string|bool\\s+(\\w+)\\s*(?=\\()");
rule.format = Function;
highlightingRules.append(rule);
// Number highlighting
rule.pattern = QRegularExpression("\\b[0-9]*\\.?[0-9]+\\b");
rule.format = Number;
highlightingRules.append(rule);
// Keyword highlighting
QStringList keywordPatterns = {
"\\bvoid\\b", "\\bint\\b", "\\bdouble\\b", "\\bstring\\b", "\\bbool\\b",
"\\bclass\\b", "\\bif\\b", "\\belse\\b", "\\bwhile\\b", "\\bfor\\b",
"\\breturn\\b", "\\bnew\\b", "\\bthis\\b", "\\bpublic\\b", "\\bprivate\\b",
"\\bprotected\\b", "\\bstatic\\b", "\\btrue\\b", "\\bfalse\\b", "\\bnull\\b",
"\\bnamespace\\b", "\\busing\\b", "\\btry\\b", "\\bcatch\\b", "\\bfinally\\b",
"\\bthrow\\b", "\\bvar\\b"
};
for (const QString &pattern : keywordPatterns) {
rule.pattern = QRegularExpression(pattern);
rule.format = Keyword;
highlightingRules.append(rule);
}
// String highlighting
rule.pattern = QRegularExpression("\".*?\"");
rule.format = String;
highlightingRules.append(rule);
// Single-line comment highlighting
rule.pattern = QRegularExpression("//[^\n]*");
rule.format = Comment;
highlightingRules.append(rule);
// Multi-line comment highlighting
rule.pattern = QRegularExpression("/\\*.*?\\*/");
rule.format = Comment;
highlightingRules.append(rule);
}
return highlightingRules;
}
static QVector<HighlightingRule> cppHighlightingRules()
{
static QVector<HighlightingRule> highlightingRules;
if (highlightingRules.isEmpty()) {
HighlightingRule rule;
rule.pattern = QRegularExpression(".*");
rule.format = Default;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\b(\\w+)\\s*(?=\\()");
rule.format = FunctionCall;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\b[a-zA-Z_][a-zA-Z0-9_]*\\s+(\\w+)\\s*\\(");
rule.format = Function;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\b[0-9]*\\.?[0-9]+\\b");
rule.format = Number;
highlightingRules.append(rule);
QStringList keywordPatterns = {
"\\bauto\\b", "\\bbool\\b", "\\bbreak\\b", "\\bcase\\b", "\\bcatch\\b",
"\\bchar\\b", "\\bclass\\b", "\\bconst\\b", "\\bconstexpr\\b", "\\bcontinue\\b",
"\\bdefault\\b", "\\bdelete\\b", "\\bdo\\b", "\\bdouble\\b", "\\belse\\b",
"\\belifdef\\b", "\\belifndef\\b", "\\bembed\\b", "\\benum\\b", "\\bexplicit\\b",
"\\bextern\\b", "\\bfalse\\b", "\\bfloat\\b", "\\bfor\\b", "\\bfriend\\b", "\\bgoto\\b",
"\\bif\\b", "\\binline\\b", "\\bint\\b", "\\blong\\b", "\\bmutable\\b", "\\bnamespace\\b",
"\\bnew\\b", "\\bnoexcept\\b", "\\bnullptr\\b", "\\boperator\\b", "\\boverride\\b",
"\\bprivate\\b", "\\bprotected\\b", "\\bpublic\\b", "\\bregister\\b", "\\breinterpret_cast\\b",
"\\breturn\\b", "\\bshort\\b", "\\bsigned\\b", "\\bsizeof\\b", "\\bstatic\\b", "\\bstatic_assert\\b",
"\\bstatic_cast\\b", "\\bstruct\\b", "\\bswitch\\b", "\\btemplate\\b", "\\bthis\\b",
"\\bthrow\\b", "\\btrue\\b", "\\btry\\b", "\\btypedef\\b", "\\btypeid\\b", "\\btypename\\b",
"\\bunion\\b", "\\bunsigned\\b", "\\busing\\b", "\\bvirtual\\b", "\\bvoid\\b",
"\\bvolatile\\b", "\\bwchar_t\\b", "\\bwhile\\b"
};
for (const QString &pattern : keywordPatterns) {
rule.pattern = QRegularExpression(pattern);
rule.format = Keyword;
highlightingRules.append(rule);
}
rule.pattern = QRegularExpression("\".*?\"");
rule.format = String;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\'.*?\'");
rule.format = String;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("//[^\n]*");
rule.format = Comment;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("/\\*.*?\\*/");
rule.format = Comment;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("#(?:include|define|undef|ifdef|ifndef|if|else|elif|endif|error|pragma)\\b.*");
rule.format = Preprocessor;
highlightingRules.append(rule);
}
return highlightingRules;
}
static QVector<HighlightingRule> typescriptHighlightingRules()
{
static QVector<HighlightingRule> highlightingRules;
if (highlightingRules.isEmpty()) {
HighlightingRule rule;
rule.pattern = QRegularExpression(".*");
rule.format = Default;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\b(\\w+)\\s*(?=\\()");
rule.format = FunctionCall;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\bfunction\\s+(\\w+)\\b");
rule.format = Function;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\b[0-9]*\\.?[0-9]+\\b");
rule.format = Number;
highlightingRules.append(rule);
QStringList keywordPatterns = {
"\\bfunction\\b", "\\bvar\\b", "\\blet\\b", "\\bconst\\b", "\\bif\\b", "\\belse\\b",
"\\bfor\\b", "\\bwhile\\b", "\\breturn\\b", "\\btry\\b", "\\bcatch\\b", "\\bfinally\\b",
"\\bthrow\\b", "\\bnew\\b", "\\bdelete\\b", "\\btypeof\\b", "\\binstanceof\\b",
"\\bdo\\b", "\\bswitch\\b", "\\bcase\\b", "\\bbreak\\b", "\\bcontinue\\b",
"\\bpublic\\b", "\\bprivate\\b", "\\bprotected\\b", "\\bstatic\\b", "\\breadonly\\b",
"\\benum\\b", "\\binterface\\b", "\\bextends\\b", "\\bimplements\\b", "\\bexport\\b",
"\\bimport\\b", "\\btype\\b", "\\bnamespace\\b", "\\babstract\\b", "\\bas\\b",
"\\basync\\b", "\\bawait\\b", "\\bclass\\b", "\\bconstructor\\b", "\\bget\\b",
"\\bset\\b", "\\bnull\\b", "\\bundefined\\b", "\\btrue\\b", "\\bfalse\\b"
};
for (const QString &pattern : keywordPatterns) {
rule.pattern = QRegularExpression(pattern);
rule.format = Keyword;
highlightingRules.append(rule);
}
QStringList typePatterns = {
"\\bstring\\b", "\\bnumber\\b", "\\bboolean\\b", "\\bany\\b", "\\bvoid\\b",
"\\bnever\\b", "\\bunknown\\b", "\\bObject\\b", "\\bArray\\b"
};
for (const QString &pattern : typePatterns) {
rule.pattern = QRegularExpression(pattern);
rule.format = Type;
highlightingRules.append(rule);
}
rule.pattern = QRegularExpression("\".*?\"|'.*?'|`.*?`");
rule.format = String;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("//[^\n]*");
rule.format = Comment;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("/\\*.*?\\*/");
rule.format = Comment;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("=>");
rule.format = Arrow;
highlightingRules.append(rule);
}
return highlightingRules;
}
static QVector<HighlightingRule> javaHighlightingRules()
{
static QVector<HighlightingRule> highlightingRules;
if (highlightingRules.isEmpty()) {
HighlightingRule rule;
rule.pattern = QRegularExpression(".*");
rule.format = Default;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\b(\\w+)\\s*(?=\\()");
rule.format = FunctionCall;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\bvoid\\s+(\\w+)\\b");
rule.format = Function;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\b[0-9]*\\.?[0-9]+\\b");
rule.format = Number;
highlightingRules.append(rule);
QStringList keywordPatterns = {
"\\bpublic\\b", "\\bprivate\\b", "\\bprotected\\b", "\\bstatic\\b", "\\bfinal\\b",
"\\bclass\\b", "\\bif\\b", "\\belse\\b", "\\bwhile\\b", "\\bfor\\b",
"\\breturn\\b", "\\bnew\\b", "\\bimport\\b", "\\bpackage\\b", "\\btry\\b",
"\\bcatch\\b", "\\bthrow\\b", "\\bthrows\\b", "\\bfinally\\b", "\\binterface\\b",
"\\bextends\\b", "\\bimplements\\b", "\\bsuper\\b", "\\bthis\\b", "\\bvoid\\b",
"\\bboolean\\b", "\\bbyte\\b", "\\bchar\\b", "\\bdouble\\b", "\\bfloat\\b",
"\\bint\\b", "\\blong\\b", "\\bshort\\b", "\\bswitch\\b", "\\bcase\\b",
"\\bdefault\\b", "\\bcontinue\\b", "\\bbreak\\b", "\\babstract\\b", "\\bassert\\b",
"\\benum\\b", "\\binstanceof\\b", "\\bnative\\b", "\\bstrictfp\\b", "\\bsynchronized\\b",
"\\btransient\\b", "\\bvolatile\\b", "\\bconst\\b", "\\bgoto\\b"
};
for (const QString &pattern : keywordPatterns) {
rule.pattern = QRegularExpression(pattern);
rule.format = Keyword;
highlightingRules.append(rule);
}
rule.pattern = QRegularExpression("\".*?\"");
rule.format = String;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\'.*?\'");
rule.format = String;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("//[^\n]*");
rule.format = Comment;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("/\\*.*?\\*/");
rule.format = Comment;
highlightingRules.append(rule);
}
return highlightingRules;
}
static QVector<HighlightingRule> goHighlightingRules()
{
static QVector<HighlightingRule> highlightingRules;
if (highlightingRules.isEmpty()) {
HighlightingRule rule;
rule.pattern = QRegularExpression(".*");
rule.format = Default;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\b(\\w+)\\s*(?=\\()");
rule.format = FunctionCall;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\bfunc\\s+(\\w+)\\b");
rule.format = Function;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\b[0-9]*\\.?[0-9]+\\b");
rule.format = Number;
highlightingRules.append(rule);
QStringList keywordPatterns = {
"\\bfunc\\b", "\\bpackage\\b", "\\bimport\\b", "\\bvar\\b", "\\bconst\\b",
"\\btype\\b", "\\bstruct\\b", "\\binterface\\b", "\\bfor\\b", "\\bif\\b",
"\\belse\\b", "\\bswitch\\b", "\\bcase\\b", "\\bdefault\\b", "\\breturn\\b",
"\\bbreak\\b", "\\bcontinue\\b", "\\bgoto\\b", "\\bfallthrough\\b",
"\\bdefer\\b", "\\bchan\\b", "\\bmap\\b", "\\brange\\b"
};
for (const QString &pattern : keywordPatterns) {
rule.pattern = QRegularExpression(pattern);
rule.format = Keyword;
highlightingRules.append(rule);
}
rule.pattern = QRegularExpression("\".*?\"");
rule.format = String;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("`.*?`");
rule.format = String;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("//[^\n]*");
rule.format = Comment;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("/\\*.*?\\*/");
rule.format = Comment;
highlightingRules.append(rule);
}
return highlightingRules;
}
static QVector<HighlightingRule> bashHighlightingRules()
{
static QVector<HighlightingRule> highlightingRules;
if (highlightingRules.isEmpty()) {
HighlightingRule rule;
rule.pattern = QRegularExpression(".*");
rule.format = Default;
highlightingRules.append(rule);
QStringList commandPatterns = {
"\\b(grep|awk|sed|ls|cat|echo|rm|mkdir|cp|break|alias|eval|cd|exec|head|tail|strings|printf|touch|mv|chmod)\\b"
};
for (const QString &pattern : commandPatterns) {
rule.pattern = QRegularExpression(pattern);
rule.format = Command;
highlightingRules.append(rule);
}
rule.pattern = QRegularExpression("\\b[0-9]*\\.?[0-9]+\\b");
rule.format = Number;
highlightingRules.append(rule);
QStringList keywordPatterns = {
"\\bif\\b", "\\bthen\\b", "\\belse\\b", "\\bfi\\b", "\\bfor\\b",
"\\bin\\b", "\\bdo\\b", "\\bdone\\b", "\\bwhile\\b", "\\buntil\\b",
"\\bcase\\b", "\\besac\\b", "\\bfunction\\b", "\\breturn\\b",
"\\blocal\\b", "\\bdeclare\\b", "\\bunset\\b", "\\bexport\\b",
"\\breadonly\\b", "\\bshift\\b", "\\bexit\\b"
};
for (const QString &pattern : keywordPatterns) {
rule.pattern = QRegularExpression(pattern);
rule.format = Keyword;
highlightingRules.append(rule);
}
rule.pattern = QRegularExpression("\".*?\"");
rule.format = String;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\'.*?\'");
rule.format = String;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\$(\\w+|\\{[^}]+\\})");
rule.format = Variable;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("#[^\n]*");
rule.format = Comment;
highlightingRules.append(rule);
}
return highlightingRules;
}
static QVector<HighlightingRule> latexHighlightingRules()
{
static QVector<HighlightingRule> highlightingRules;
if (highlightingRules.isEmpty()) {
HighlightingRule rule;
rule.pattern = QRegularExpression(".*");
rule.format = Default;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\\\[A-Za-z]+"); // Pattern for LaTeX commands
rule.format = Command;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("%[^\n]*"); // Pattern for LaTeX comments
rule.format = Comment;
highlightingRules.append(rule);
}
return highlightingRules;
}
static QVector<HighlightingRule> htmlHighlightingRules()
{
static QVector<HighlightingRule> highlightingRules;
if (highlightingRules.isEmpty()) {
HighlightingRule rule;
rule.pattern = QRegularExpression(".*");
rule.format = Default;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\b(\\w+)\\s*=");
rule.format = AttributeName;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\".*?\"|'.*?'");
rule.format = AttributeValue;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("<!--.*?-->");
rule.format = Comment;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("&[a-zA-Z0-9#]*;");
rule.format = SpecialCharacter;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("<!DOCTYPE.*?>");
rule.format = DocType;
highlightingRules.append(rule);
}
return highlightingRules;
}
static QVector<HighlightingRule> phpHighlightingRules()
{
static QVector<HighlightingRule> highlightingRules;
if (highlightingRules.isEmpty()) {
HighlightingRule rule;
rule.pattern = QRegularExpression(".*");
rule.format = Default;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\b(\\w+)\\s*(?=\\()");
rule.format = FunctionCall;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\bfunction\\s+(\\w+)\\b");
rule.format = Function;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\\b[0-9]*\\.?[0-9]+\\b");
rule.format = Number;
highlightingRules.append(rule);
QStringList keywordPatterns = {
"\\bif\\b", "\\belse\\b", "\\belseif\\b", "\\bwhile\\b", "\\bfor\\b",
"\\bforeach\\b", "\\breturn\\b", "\\bprint\\b", "\\binclude\\b", "\\brequire\\b",
"\\binclude_once\\b", "\\brequire_once\\b", "\\btry\\b", "\\bcatch\\b",
"\\bfinally\\b", "\\bcontinue\\b", "\\bbreak\\b", "\\bclass\\b", "\\bfunction\\b",
"\\bnew\\b", "\\bthrow\\b", "\\barray\\b", "\\bpublic\\b", "\\bprivate\\b",
"\\bprotected\\b", "\\bstatic\\b", "\\bglobal\\b", "\\bisset\\b", "\\bunset\\b",
"\\bnull\\b", "\\btrue\\b", "\\bfalse\\b"
};
for (const QString &pattern : keywordPatterns) {
rule.pattern = QRegularExpression(pattern);
rule.format = Keyword;
highlightingRules.append(rule);
}
rule.pattern = QRegularExpression("\".*?\"");
rule.format = String;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("\'.*?\'");
rule.format = String;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("//[^\n]*");
rule.format = Comment;
highlightingRules.append(rule);
rule.pattern = QRegularExpression("/\\*.*?\\*/");
rule.format = Comment;
highlightingRules.append(rule);
}
return highlightingRules;
}
static QVector<HighlightingRule> jsonHighlightingRules()
{
static QVector<HighlightingRule> highlightingRules;
if (highlightingRules.isEmpty()) {
HighlightingRule rule;
rule.pattern = QRegularExpression(".*");
rule.format = Default;
highlightingRules.append(rule);
// Key string rule
rule.pattern = QRegularExpression("\".*?\":"); // keys are typically in the "key": format
rule.format = Key;
highlightingRules.append(rule);
// Value string rule
rule.pattern = QRegularExpression(":\\s*(\".*?\")"); // values are typically in the : "value" format
rule.format = Value;
highlightingRules.append(rule);
}
return highlightingRules;
}
SyntaxHighlighter::SyntaxHighlighter(QObject *parent)
: QSyntaxHighlighter(parent)
{
}
SyntaxHighlighter::~SyntaxHighlighter()
{
}
void SyntaxHighlighter::highlightBlock(const QString &text)
{
QTextBlock block = this->currentBlock();
// Search the first block of the frame we're in for the code to use for highlighting
int userState = block.userState();
if (QTextFrame *frame = block.document()->frameAt(block.position())) {
QTextBlock firstBlock = frame->begin().currentBlock();
if (firstBlock.isValid())
userState = firstBlock.userState();
}
QVector<HighlightingRule> rules;
switch (userState) {
case Python:
rules = pythonHighlightingRules(); break;
case Cpp:
rules = cppHighlightingRules(); break;
case Csharp:
rules = csharpHighlightingRules(); break;
case Bash:
rules = bashHighlightingRules(); break;
case TypeScript:
rules = typescriptHighlightingRules(); break;
case Java:
rules = javaHighlightingRules(); break;
case Go:
rules = javaHighlightingRules(); break;
case Json:
rules = jsonHighlightingRules(); break;
case Latex:
rules = latexHighlightingRules(); break;
case Html:
rules = htmlHighlightingRules(); break;
case Php:
rules = phpHighlightingRules(); break;
default: break;
}
for (const HighlightingRule &rule : std::as_const(rules)) {
QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text);
while (matchIterator.hasNext()) {
QRegularExpressionMatch match = matchIterator.next();
int startIndex = match.capturedStart();
int length = match.capturedLength();
QTextCharFormat format;
format.setForeground(formatToColor(rule.format, m_codeColors));
setFormat(startIndex, length, format);
}
}
}
// TODO (Adam) This class replaces characters in the text in order to provide markup and syntax highlighting
// which destroys the original text in favor of the replaced text. This is a problem when we select
// text and then the user tries to 'copy' the text: the original text should be placed in the clipboard
// not the replaced text. A possible solution is to have this class keep a mapping of the original
// indices and the replacement indices and then use the original text that is stored in memory in the
// chat class to populate the clipboard.
ChatViewTextProcessor::ChatViewTextProcessor(QObject *parent)
: QObject{parent}
, m_textDocument(nullptr)
, m_syntaxHighlighter(new SyntaxHighlighter(this))
, m_isProcessingText(false)
, m_shouldProcessText(true)
, m_fontPixelSize(QGuiApplication::font().pointSizeF())
{
}
QQuickTextDocument* ChatViewTextProcessor::textDocument() const
{
return m_textDocument;
}
void ChatViewTextProcessor::setTextDocument(QQuickTextDocument* textDocument)
{
if (m_textDocument)
disconnect(m_textDocument->textDocument(), &QTextDocument::contentsChanged, this, &ChatViewTextProcessor::handleTextChanged);
m_textDocument = textDocument;
m_syntaxHighlighter->setDocument(m_textDocument->textDocument());
connect(m_textDocument->textDocument(), &QTextDocument::contentsChanged, this, &ChatViewTextProcessor::handleTextChanged);
handleTextChanged();
}
bool ChatViewTextProcessor::tryCopyAtPosition(int position) const
{
for (const auto &copy : m_copies) {
if (position >= copy.startPos && position <= copy.endPos) {
QClipboard *clipboard = QGuiApplication::clipboard();
clipboard->setText(copy.text);
return true;
}
}
return false;
}
bool ChatViewTextProcessor::shouldProcessText() const
{
return m_shouldProcessText;
}
void ChatViewTextProcessor::setShouldProcessText(bool b)
{
if (m_shouldProcessText == b)
return;
m_shouldProcessText = b;
emit shouldProcessTextChanged();
handleTextChanged();
}
qreal ChatViewTextProcessor::fontPixelSize() const
{
return m_fontPixelSize;
}
void ChatViewTextProcessor::setFontPixelSize(qreal sz)
{
if (m_fontPixelSize == sz)
return;
m_fontPixelSize = sz;
emit fontPixelSizeChanged();
handleTextChanged();
}
CodeColors ChatViewTextProcessor::codeColors() const
{
return m_syntaxHighlighter->codeColors();
}
void ChatViewTextProcessor::setCodeColors(const CodeColors &colors)
{
m_syntaxHighlighter->setCodeColors(colors);
emit codeColorsChanged();
}
void traverseDocument(QTextDocument *doc, QTextFrame *frame)
{
QTextFrame *rootFrame = frame ? frame : doc->rootFrame();
QTextFrame::iterator rootIt;
if (!frame)
qDebug() << "Begin traverse";
for (rootIt = rootFrame->begin(); !rootIt.atEnd(); ++rootIt) {
QTextFrame *childFrame = rootIt.currentFrame();
QTextBlock childBlock = rootIt.currentBlock();
if (childFrame) {
qDebug() << "Frame from" << childFrame->firstPosition() << "to" << childFrame->lastPosition();
traverseDocument(doc, childFrame);
} else if (childBlock.isValid()) {
qDebug() << QString(" Block %1 position:").arg(childBlock.userState()) << childBlock.position();
qDebug() << QString(" Block %1 text:").arg(childBlock.userState()) << childBlock.text();
// Iterate over lines within the block
for (QTextBlock::iterator blockIt = childBlock.begin(); !(blockIt.atEnd()); ++blockIt) {
QTextFragment fragment = blockIt.fragment();
if (fragment.isValid()) {
qDebug() << " Fragment text:" << fragment.text();
}
}
}
}
if (!frame)
qDebug() << "End traverse";
}
void ChatViewTextProcessor::handleTextChanged()
{
if (!m_textDocument || m_isProcessingText || !m_shouldProcessText)
return;
m_isProcessingText = true;
// Force full layout of the text document to work around a bug in Qt
// TODO(jared): report the Qt bug and link to the report here
QTextDocument* doc = m_textDocument->textDocument();
(void)doc->documentLayout()->documentSize();
handleCodeBlocks();
handleMarkdown();
// We insert an invisible char at the end to make sure the document goes back to the default
// text format
QTextCursor cursor(doc);
QString invisibleCharacter = QString(QChar(0xFEFF));
cursor.insertText(invisibleCharacter, QTextCharFormat());
m_syntaxHighlighter->rehighlight();
m_isProcessingText = false;
}
void ChatViewTextProcessor::handleCodeBlocks()
{
QTextDocument* doc = m_textDocument->textDocument();
QTextCursor cursor(doc);
QTextCharFormat textFormat;
textFormat.setFontFamilies(QStringList() << "Monospace");
textFormat.setForeground(QColor("white"));
QTextFrameFormat frameFormatBase;
frameFormatBase.setBackground(codeColors().backgroundColor);
QTextTableFormat tableFormat;
tableFormat.setMargin(0);
tableFormat.setPadding(0);
tableFormat.setBorder(0);
tableFormat.setBorderCollapse(true);
QList<QTextLength> constraints;
constraints << QTextLength(QTextLength::PercentageLength, 100);
tableFormat.setColumnWidthConstraints(constraints);
QTextTableFormat headerTableFormat;
headerTableFormat.setBackground(codeColors().headerColor);
headerTableFormat.setPadding(0);
headerTableFormat.setBorder(0);
headerTableFormat.setBorderCollapse(true);
headerTableFormat.setTopMargin(10);
headerTableFormat.setBottomMargin(10);
headerTableFormat.setLeftMargin(15);
headerTableFormat.setRightMargin(15);
QList<QTextLength> headerConstraints;
headerConstraints << QTextLength(QTextLength::PercentageLength, 80);
headerConstraints << QTextLength(QTextLength::PercentageLength, 20);
headerTableFormat.setColumnWidthConstraints(headerConstraints);
QTextTableFormat codeBlockTableFormat;
codeBlockTableFormat.setBackground(codeColors().backgroundColor);
codeBlockTableFormat.setPadding(0);
codeBlockTableFormat.setBorder(0);
codeBlockTableFormat.setBorderCollapse(true);
codeBlockTableFormat.setTopMargin(15);
codeBlockTableFormat.setBottomMargin(15);
codeBlockTableFormat.setLeftMargin(15);
codeBlockTableFormat.setRightMargin(15);
codeBlockTableFormat.setColumnWidthConstraints(constraints);
QTextImageFormat copyImageFormat;
copyImageFormat.setWidth(24);
copyImageFormat.setHeight(24);
copyImageFormat.setName("qrc:/gpt4all/icons/copy.svg");
// Regex for code blocks
static const QRegularExpression reCode("```(.*?)(```|$)", QRegularExpression::DotMatchesEverythingOption);
QRegularExpressionMatchIterator iCode = reCode.globalMatch(doc->toPlainText());
QList<QRegularExpressionMatch> matchesCode;
while (iCode.hasNext())
matchesCode.append(iCode.next());
QVector<CodeCopy> newCopies;
QVector<QTextFrame*> frames;
for(int index = matchesCode.count() - 1; index >= 0; --index) {
cursor.setPosition(matchesCode[index].capturedStart());
cursor.setPosition(matchesCode[index].capturedEnd(), QTextCursor::KeepAnchor);
cursor.removeSelectedText();
int startPos = cursor.position();
QTextFrameFormat frameFormat = frameFormatBase;
QString capturedText = matchesCode[index].captured(1);
QString codeLanguage;
QStringList lines = capturedText.split('\n');
if (lines.last().isEmpty()) {
lines.removeLast();
}
if (lines.count() >= 2) {
const auto &firstWord = lines.first();
if (firstWord == "python"
|| firstWord == "cpp"
|| firstWord == "c++"
|| firstWord == "csharp"
|| firstWord == "c#"
|| firstWord == "c"
|| firstWord == "bash"
|| firstWord == "javascript"
|| firstWord == "typescript"
|| firstWord == "java"
|| firstWord == "go"
|| firstWord == "golang"
|| firstWord == "json"
|| firstWord == "latex"
|| firstWord == "html"
|| firstWord == "php") {
codeLanguage = firstWord;
}
lines.removeFirst();
}
QTextFrame *mainFrame = cursor.currentFrame();
cursor.setCharFormat(textFormat);
QTextFrame *frame = cursor.insertFrame(frameFormat);
QTextTable *table = cursor.insertTable(codeLanguage.isEmpty() ? 1 : 2, 1, tableFormat);
if (!codeLanguage.isEmpty()) {
QTextTableCell headerCell = table->cellAt(0, 0);
QTextCursor headerCellCursor = headerCell.firstCursorPosition();
QTextTable *headerTable = headerCellCursor.insertTable(1, 2, headerTableFormat);
QTextTableCell header = headerTable->cellAt(0, 0);
QTextCursor headerCursor = header.firstCursorPosition();
headerCursor.insertText(codeLanguage);
QTextTableCell copy = headerTable->cellAt(0, 1);
QTextCursor copyCursor = copy.firstCursorPosition();
int startPos = copyCursor.position();
CodeCopy newCopy;
newCopy.text = lines.join("\n");
newCopy.startPos = copyCursor.position();
newCopy.endPos = newCopy.startPos + 1;
newCopies.append(newCopy);
// FIXME: There are two reasons this is commented out. Odd drawing behavior is seen when this is added
// and one selects with the mouse the code language in a code block. The other reason is the code that
// tries to do a hit test for the image is just very broken and buggy and does not always work. So I'm
// disabling this code and included functionality for v3.0.0 until I can figure out how to make this much
// less buggy
#if 0
// QTextBlockFormat blockFormat;
// blockFormat.setAlignment(Qt::AlignRight);
// copyCursor.setBlockFormat(blockFormat);
// copyCursor.insertImage(copyImageFormat, QTextFrameFormat::FloatRight);
#endif
}
QTextTableCell codeCell = table->cellAt(codeLanguage.isEmpty() ? 0 : 1, 0);
QTextCursor codeCellCursor = codeCell.firstCursorPosition();
QTextTable *codeTable = codeCellCursor.insertTable(1, 1, codeBlockTableFormat);
QTextTableCell code = codeTable->cellAt(0, 0);
QTextCharFormat codeBlockCharFormat;
codeBlockCharFormat.setForeground(codeColors().defaultColor);
QFont monospaceFont("Courier");
monospaceFont.setPointSize(m_fontPixelSize);
if (monospaceFont.family() != "Courier") {
monospaceFont.setFamily("Monospace"); // Fallback if Courier isn't available
}
QTextCursor codeCursor = code.firstCursorPosition();
codeBlockCharFormat.setFont(monospaceFont); // Update the font for the codeblock
codeCursor.setCharFormat(codeBlockCharFormat);
codeCursor.block().setUserState(stringToLanguage(codeLanguage));
codeCursor.insertText(lines.join('\n'));
cursor = mainFrame->lastCursorPosition();
cursor.setCharFormat(QTextCharFormat());
}
m_copies = newCopies;
}
void replaceAndInsertMarkdown(int startIndex, int endIndex, QTextDocument *doc)
{
QTextCursor cursor(doc);
cursor.setPosition(startIndex);
cursor.setPosition(endIndex, QTextCursor::KeepAnchor);
QTextDocumentFragment fragment(cursor);
const QString plainText = fragment.toPlainText();
cursor.removeSelectedText();
QTextDocument::MarkdownFeatures features = static_cast<QTextDocument::MarkdownFeatures>(
QTextDocument::MarkdownNoHTML | QTextDocument::MarkdownDialectGitHub);
cursor.insertMarkdown(plainText, features);
cursor.block().setUserState(Markdown);
}
void ChatViewTextProcessor::handleMarkdown()
{
QTextDocument* doc = m_textDocument->textDocument();
QTextCursor cursor(doc);
QVector<QPair<int, int>> codeBlockPositions;
QTextFrame *rootFrame = doc->rootFrame();
QTextFrame::iterator rootIt;
bool hasAlreadyProcessedMarkdown = false;
for (rootIt = rootFrame->begin(); !rootIt.atEnd(); ++rootIt) {
QTextFrame *childFrame = rootIt.currentFrame();
QTextBlock childBlock = rootIt.currentBlock();
if (childFrame) {
codeBlockPositions.append(qMakePair(childFrame->firstPosition()-1, childFrame->lastPosition()+1));
for (QTextFrame::iterator frameIt = childFrame->begin(); !frameIt.atEnd(); ++frameIt) {
QTextBlock block = frameIt.currentBlock();
if (block.isValid() && block.userState() == Markdown)
hasAlreadyProcessedMarkdown = true;
}
} else if (childBlock.isValid() && childBlock.userState() == Markdown)
hasAlreadyProcessedMarkdown = true;
}
if (!hasAlreadyProcessedMarkdown) {
std::sort(codeBlockPositions.begin(), codeBlockPositions.end(), [](const QPair<int, int> &a, const QPair<int, int> &b) {
return a.first > b.first;
});
int lastIndex = doc->characterCount() - 1;
for (const auto &pos : codeBlockPositions) {
int nonCodeStart = pos.second;
int nonCodeEnd = lastIndex;
if (nonCodeEnd > nonCodeStart) {
replaceAndInsertMarkdown(nonCodeStart, nonCodeEnd, doc);
}
lastIndex = pos.first;
}
if (lastIndex > 0)
replaceAndInsertMarkdown(0, lastIndex, doc);
}
}