diff --git a/CMakeLists.txt b/CMakeLists.txt index a89c7d06..7490196b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,7 @@ set(SQLB_MOC_HDR src/VacuumDialog.h src/sqlitetablemodel.h src/sqltextedit.h + src/jsontextedit.h src/DbStructureModel.h src/Application.h src/CipherDialog.h @@ -139,6 +140,7 @@ set(SQLB_SRC src/sqlitetablemodel.cpp src/sqlitetypes.cpp src/sqltextedit.cpp + src/jsontextedit.cpp src/csvparser.cpp src/DbStructureModel.cpp src/grammar/Sqlite3Lexer.cpp diff --git a/libs/qscintilla/Qt4Qt5/CMakeLists.txt b/libs/qscintilla/Qt4Qt5/CMakeLists.txt index a4bef52a..a90f00b8 100644 --- a/libs/qscintilla/Qt4Qt5/CMakeLists.txt +++ b/libs/qscintilla/Qt4Qt5/CMakeLists.txt @@ -22,6 +22,7 @@ set(QSCINTILLA_SRC qscilexer.cpp qscilexercustom.cpp qscilexersql.cpp + qscilexerjson.cpp qscimacro.cpp qsciprinter.cpp qscistyle.cpp @@ -33,6 +34,7 @@ set(QSCINTILLA_SRC PlatQt.cpp ScintillaQt.cpp ../lexers/LexSQL.cpp + ../lexers/LexJSON.cpp ../lexlib/Accessor.cpp ../lexlib/CharacterCategory.cpp ../lexlib/CharacterSet.cpp @@ -143,6 +145,7 @@ set(QSCINTILLA_MOC_HDR ./Qsci/qscilexer.h ./Qsci/qscilexercustom.h ./Qsci/qscilexersql.h + ./Qsci/qscilexerjson.h ./Qsci/qscimacro.h SciClasses.h ScintillaQt.h diff --git a/libs/qscintilla/Qt4Qt5/qscilexerjson.cpp b/libs/qscintilla/Qt4Qt5/qscilexerjson.cpp new file mode 100644 index 00000000..09385d30 --- /dev/null +++ b/libs/qscintilla/Qt4Qt5/qscilexerjson.cpp @@ -0,0 +1,298 @@ +// This module implements the QsciLexerJSON class. +// +// Copyright (c) 2017 Riverbank Computing Limited +// +// This file is part of QScintilla. +// +// This file may be used under the terms of the GNU General Public License +// version 3.0 as published by the Free Software Foundation and appearing in +// the file LICENSE included in the packaging of this file. Please review the +// following information to ensure the GNU General Public License version 3.0 +// requirements will be met: http://www.gnu.org/copyleft/gpl.html. +// +// If you do not wish to use this file under the terms of the GPL version 3.0 +// then you may purchase a commercial license. For more information contact +// info@riverbankcomputing.com. +// +// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +// WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + + +#include "Qsci/qscilexerjson.h" + +#include +#include +#include + + +// The ctor. +QsciLexerJSON::QsciLexerJSON(QObject *parent) + : QsciLexer(parent), + allow_comments(true), escape_sequence(true), fold_compact(true) +{ +} + + +// The dtor. +QsciLexerJSON::~QsciLexerJSON() +{ +} + + +// Returns the language name. +const char *QsciLexerJSON::language() const +{ + return "JSON"; +} + + +// Returns the lexer name. +const char *QsciLexerJSON::lexer() const +{ + return "json"; +} + + +// Returns the foreground colour of the text for a style. +QColor QsciLexerJSON::defaultColor(int style) const +{ + switch (style) + { + case UnclosedString: + case Error: + return QColor(0xff, 0xff, 0xff); + + case Number: + return QColor(0x00, 0x7f, 0x7f); + + case String: + return QColor(0x7f, 0x00, 0x00); + + case Property: + return QColor(0x88, 0x0a, 0xe8); + + case EscapeSequence: + return QColor(0x0b, 0x98, 0x2e); + + case CommentLine: + case CommentBlock: + return QColor(0x05, 0xbb, 0xae); + + case Operator: + return QColor(0x18, 0x64, 0x4a); + + case IRI: + return QColor(0x00, 0x00, 0xff); + + case IRICompact: + return QColor(0xd1, 0x37, 0xc1); + + case Keyword: + return QColor(0x0b, 0xce, 0xa7); + + case KeywordLD: + return QColor(0xec, 0x28, 0x06); + } + + return QsciLexer::defaultColor(style); +} + + +// Returns the end-of-line fill for a style. +bool QsciLexerJSON::defaultEolFill(int style) const +{ + switch (style) + { + case UnclosedString: + return true; + } + + return QsciLexer::defaultEolFill(style); +} + + +// Returns the font of the text for a style. +QFont QsciLexerJSON::defaultFont(int style) const +{ + QFont f; + + switch (style) + { + case CommentLine: + f = QsciLexer::defaultFont(style); + f.setItalic(true); + break; + + case Keyword: + f = QsciLexer::defaultFont(style); + f.setBold(true); + break; + + default: + f = QsciLexer::defaultFont(style); + } + + return f; +} + + +// Returns the set of keywords. +const char *QsciLexerJSON::keywords(int set) const +{ + if (set == 1) + return "false true null"; + + if (set == 2) + return + "@id @context @type @value @language @container @list @set " + "@reverse @index @base @vocab @graph"; + + return 0; +} + + +// Returns the user name of a style. +QString QsciLexerJSON::description(int style) const +{ + switch (style) + { + case Default: + return tr("Default"); + + case Number: + return tr("Number"); + + case String: + return tr("String"); + + case UnclosedString: + return tr("Unclosed string"); + + case Property: + return tr("Property"); + + case EscapeSequence: + return tr("Escape sequence"); + + case CommentLine: + return tr("Line comment"); + + case CommentBlock: + return tr("Block comment"); + + case Operator: + return tr("Operator"); + + case IRI: + return tr("IRI"); + + case IRICompact: + return tr("JSON-LD compact IRI"); + + case Keyword: + return tr("JSON keyword"); + + case KeywordLD: + return tr("JSON-LD keyword"); + + case Error: + return tr("Parsing error"); + } + + return QString(); +} + + +// Returns the background colour of the text for a style. +QColor QsciLexerJSON::defaultPaper(int style) const +{ + switch (style) + { + case UnclosedString: + case Error: + return QColor(0xff, 0x00, 0x00); + } + + return QsciLexer::defaultPaper(style); +} + + +// Refresh all properties. +void QsciLexerJSON::refreshProperties() +{ + setAllowCommentsProp(); + setEscapeSequenceProp(); + setCompactProp(); +} + + +// Read properties from the settings. +bool QsciLexerJSON::readProperties(QSettings &qs,const QString &prefix) +{ + allow_comments = qs.value(prefix + "allowcomments", true).toBool(); + escape_sequence = qs.value(prefix + "escapesequence", true).toBool(); + fold_compact = qs.value(prefix + "foldcompact", true).toBool(); + + return true; +} + + +// Write properties to the settings. +bool QsciLexerJSON::writeProperties(QSettings &qs,const QString &prefix) const +{ + qs.setValue(prefix + "allowcomments", allow_comments); + qs.setValue(prefix + "escapesequence", escape_sequence); + qs.setValue(prefix + "foldcompact", fold_compact); + + return true; +} + + +// Set if comments are highlighted +void QsciLexerJSON::setHighlightComments(bool highlight) +{ + allow_comments = highlight; + + setAllowCommentsProp(); +} + + +// Set the "lexer.json.allow.comments" property. +void QsciLexerJSON::setAllowCommentsProp() +{ + emit propertyChanged("lexer.json.allow.comments", + (allow_comments ? "1" : "0")); +} + + +// Set if escape sequences are highlighted. +void QsciLexerJSON::setHighlightEscapeSequences(bool highlight) +{ + escape_sequence = highlight; + + setEscapeSequenceProp(); +} + + +// Set the "lexer.json.escape.sequence" property. +void QsciLexerJSON::setEscapeSequenceProp() +{ + emit propertyChanged("lexer.json.escape.sequence", + (escape_sequence ? "1" : "0")); +} + + +// Set if folds are compact. +void QsciLexerJSON::setFoldCompact(bool fold) +{ + fold_compact = fold; + + setCompactProp(); +} + + +// Set the "fold.compact" property. +void QsciLexerJSON::setCompactProp() +{ + emit propertyChanged("fold.compact", (fold_compact ? "1" : "0")); +} diff --git a/libs/qscintilla/Qt4Qt5/qscintilla.pro b/libs/qscintilla/Qt4Qt5/qscintilla.pro index 4b3deef9..127f249a 100644 --- a/libs/qscintilla/Qt4Qt5/qscintilla.pro +++ b/libs/qscintilla/Qt4Qt5/qscintilla.pro @@ -86,6 +86,7 @@ HEADERS = \ ./Qsci/qscilexer.h \ ./Qsci/qscilexercustom.h \ ./Qsci/qscilexersql.h \ + ./Qsci/qscilexerjson.h \ ./Qsci/qscimacro.h \ ./Qsci/qsciprinter.h \ ./Qsci/qscistyle.h \ @@ -158,6 +159,7 @@ SOURCES = \ qscilexer.cpp \ qscilexercustom.cpp \ qscilexersql.cpp \ + qscilexerjson.cpp \ qscimacro.cpp \ qsciprinter.cpp \ qscistyle.cpp \ @@ -169,6 +171,7 @@ SOURCES = \ PlatQt.cpp \ ScintillaQt.cpp \ ../lexers/LexSQL.cpp \ + ../lexers/LexJSON.cpp \ ../lexlib/Accessor.cpp \ ../lexlib/CharacterCategory.cpp \ ../lexlib/CharacterSet.cpp \ diff --git a/libs/qscintilla/lexers/LexJSON.cpp b/libs/qscintilla/lexers/LexJSON.cpp new file mode 100644 index 00000000..9c044e52 --- /dev/null +++ b/libs/qscintilla/lexers/LexJSON.cpp @@ -0,0 +1,497 @@ +// Scintilla source code edit control +/** + * @file LexJSON.cxx + * @date February 19, 2016 + * @brief Lexer for JSON and JSON-LD formats + * @author nkmathew + * + * The License.txt file describes the conditions under which this software may + * be distributed. + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "ILexer.h" +#include "Scintilla.h" +#include "SciLexer.h" +#include "WordList.h" +#include "LexAccessor.h" +#include "StyleContext.h" +#include "CharacterSet.h" +#include "LexerModule.h" +#include "OptionSet.h" + +#ifdef SCI_NAMESPACE +using namespace Scintilla; +#endif + +static const char *const JSONWordListDesc[] = { + "JSON Keywords", + "JSON-LD Keywords", + 0 +}; + +/** + * Used to detect compact IRI/URLs in JSON-LD without first looking ahead for the + * colon separating the prefix and suffix + * + * https://www.w3.org/TR/json-ld/#dfn-compact-iri + */ +struct CompactIRI { + int colonCount; + bool foundInvalidChar; + CharacterSet setCompactIRI; + CompactIRI() { + colonCount = 0; + foundInvalidChar = false; + setCompactIRI = CharacterSet(CharacterSet::setAlpha, "$_-"); + } + void resetState() { + colonCount = 0; + foundInvalidChar = false; + } + void checkChar(int ch) { + if (ch == ':') { + colonCount++; + } else { + foundInvalidChar |= !setCompactIRI.Contains(ch); + } + } + bool shouldHighlight() const { + return !foundInvalidChar && colonCount == 1; + } +}; + +/** + * Keeps track of escaped characters in strings as per: + * + * https://tools.ietf.org/html/rfc7159#section-7 + */ +struct EscapeSequence { + int digitsLeft; + CharacterSet setHexDigits; + CharacterSet setEscapeChars; + EscapeSequence() { + digitsLeft = 0; + setHexDigits = CharacterSet(CharacterSet::setDigits, "ABCDEFabcdef"); + setEscapeChars = CharacterSet(CharacterSet::setNone, "\\\"tnbfru/"); + } + // Returns true if the following character is a valid escaped character + bool newSequence(int nextChar) { + digitsLeft = 0; + if (nextChar == 'u') { + digitsLeft = 5; + } else if (!setEscapeChars.Contains(nextChar)) { + return false; + } + return true; + } + bool atEscapeEnd() const { + return digitsLeft <= 0; + } + bool isInvalidChar(int currChar) const { + return !setHexDigits.Contains(currChar); + } +}; + +struct OptionsJSON { + bool foldCompact; + bool fold; + bool allowComments; + bool escapeSequence; + OptionsJSON() { + foldCompact = false; + fold = false; + allowComments = false; + escapeSequence = false; + } +}; + +struct OptionSetJSON : public OptionSet { + OptionSetJSON() { + DefineProperty("lexer.json.escape.sequence", &OptionsJSON::escapeSequence, + "Set to 1 to enable highlighting of escape sequences in strings"); + + DefineProperty("lexer.json.allow.comments", &OptionsJSON::allowComments, + "Set to 1 to enable highlighting of line/block comments in JSON"); + + DefineProperty("fold.compact", &OptionsJSON::foldCompact); + DefineProperty("fold", &OptionsJSON::fold); + DefineWordListSets(JSONWordListDesc); + } +}; + +class LexerJSON : public ILexer { + OptionsJSON options; + OptionSetJSON optSetJSON; + EscapeSequence escapeSeq; + WordList keywordsJSON; + WordList keywordsJSONLD; + CharacterSet setOperators; + CharacterSet setURL; + CharacterSet setKeywordJSONLD; + CharacterSet setKeywordJSON; + CompactIRI compactIRI; + + static bool IsNextNonWhitespace(LexAccessor &styler, Sci_Position start, char ch) { + Sci_Position i = 0; + while (i < 50) { + i++; + char curr = styler.SafeGetCharAt(start+i, '\0'); + char next = styler.SafeGetCharAt(start+i+1, '\0'); + bool atEOL = (curr == '\r' && next != '\n') || (curr == '\n'); + if (curr == ch) { + return true; + } else if (!isspacechar(curr) || atEOL) { + return false; + } + } + return false; + } + + /** + * Looks for the colon following the end quote + * + * Assumes property names of lengths no longer than a 100 characters. + * The colon is also expected to be less than 50 spaces after the end + * quote for the string to be considered a property name + */ + static bool AtPropertyName(LexAccessor &styler, Sci_Position start) { + Sci_Position i = 0; + bool escaped = false; + while (i < 100) { + i++; + char curr = styler.SafeGetCharAt(start+i, '\0'); + if (escaped) { + escaped = false; + continue; + } + escaped = curr == '\\'; + if (curr == '"') { + return IsNextNonWhitespace(styler, start+i, ':'); + } else if (!curr) { + return false; + } + } + return false; + } + + static bool IsNextWordInList(WordList &keywordList, CharacterSet wordSet, + StyleContext &context, LexAccessor &styler) { + char word[51]; + Sci_Position currPos = (Sci_Position) context.currentPos; + int i = 0; + while (i < 50) { + char ch = styler.SafeGetCharAt(currPos + i); + if (!wordSet.Contains(ch)) { + break; + } + word[i] = ch; + i++; + } + word[i] = '\0'; + return keywordList.InList(word); + } + + public: + LexerJSON() : + setOperators(CharacterSet::setNone, "[{}]:,"), + setURL(CharacterSet::setAlphaNum, "-._~:/?#[]@!$&'()*+,),="), + setKeywordJSONLD(CharacterSet::setAlpha, ":@"), + setKeywordJSON(CharacterSet::setAlpha, "$_") { + } + virtual ~LexerJSON() {} + virtual int SCI_METHOD Version() const { + return lvOriginal; + } + virtual void SCI_METHOD Release() { + delete this; + } + virtual const char *SCI_METHOD PropertyNames() { + return optSetJSON.PropertyNames(); + } + virtual int SCI_METHOD PropertyType(const char *name) { + return optSetJSON.PropertyType(name); + } + virtual const char *SCI_METHOD DescribeProperty(const char *name) { + return optSetJSON.DescribeProperty(name); + } + virtual Sci_Position SCI_METHOD PropertySet(const char *key, const char *val) { + if (optSetJSON.PropertySet(&options, key, val)) { + return 0; + } + return -1; + } + virtual Sci_Position SCI_METHOD WordListSet(int n, const char *wl) { + WordList *wordListN = 0; + switch (n) { + case 0: + wordListN = &keywordsJSON; + break; + case 1: + wordListN = &keywordsJSONLD; + break; + } + Sci_Position firstModification = -1; + if (wordListN) { + WordList wlNew; + wlNew.Set(wl); + if (*wordListN != wlNew) { + wordListN->Set(wl); + firstModification = 0; + } + } + return firstModification; + } + virtual void *SCI_METHOD PrivateCall(int, void *) { + return 0; + } + static ILexer *LexerFactoryJSON() { + return new LexerJSON; + } + virtual const char *SCI_METHOD DescribeWordListSets() { + return optSetJSON.DescribeWordListSets(); + } + virtual void SCI_METHOD Lex(Sci_PositionU startPos, + Sci_Position length, + int initStyle, + IDocument *pAccess); + virtual void SCI_METHOD Fold(Sci_PositionU startPos, + Sci_Position length, + int initStyle, + IDocument *pAccess); +}; + +void SCI_METHOD LexerJSON::Lex(Sci_PositionU startPos, + Sci_Position length, + int initStyle, + IDocument *pAccess) { + LexAccessor styler(pAccess); + StyleContext context(startPos, length, initStyle, styler); + int stringStyleBefore = SCE_JSON_STRING; + while (context.More()) { + switch (context.state) { + case SCE_JSON_BLOCKCOMMENT: + if (context.Match("*/")) { + context.Forward(); + context.ForwardSetState(SCE_JSON_DEFAULT); + } + break; + case SCE_JSON_LINECOMMENT: + if (context.atLineEnd) { + context.SetState(SCE_JSON_DEFAULT); + } + break; + case SCE_JSON_STRINGEOL: + if (context.atLineStart) { + context.SetState(SCE_JSON_DEFAULT); + } + break; + case SCE_JSON_ESCAPESEQUENCE: + escapeSeq.digitsLeft--; + if (!escapeSeq.atEscapeEnd()) { + if (escapeSeq.isInvalidChar(context.ch)) { + context.SetState(SCE_JSON_ERROR); + } + break; + } + if (context.ch == '"') { + context.SetState(stringStyleBefore); + context.ForwardSetState(SCE_C_DEFAULT); + } else if (context.ch == '\\') { + if (!escapeSeq.newSequence(context.chNext)) { + context.SetState(SCE_JSON_ERROR); + } + context.Forward(); + } else { + context.SetState(stringStyleBefore); + if (context.atLineEnd) { + context.ChangeState(SCE_JSON_STRINGEOL); + } + } + break; + case SCE_JSON_PROPERTYNAME: + case SCE_JSON_STRING: + if (context.ch == '"') { + if (compactIRI.shouldHighlight()) { + context.ChangeState(SCE_JSON_COMPACTIRI); + context.ForwardSetState(SCE_JSON_DEFAULT); + compactIRI.resetState(); + } else { + context.ForwardSetState(SCE_JSON_DEFAULT); + } + } else if (context.atLineEnd) { + context.ChangeState(SCE_JSON_STRINGEOL); + } else if (context.ch == '\\') { + stringStyleBefore = context.state; + if (options.escapeSequence) { + context.SetState(SCE_JSON_ESCAPESEQUENCE); + if (!escapeSeq.newSequence(context.chNext)) { + context.SetState(SCE_JSON_ERROR); + } + } + context.Forward(); + } else if (context.Match("https://") || + context.Match("http://") || + context.Match("ssh://") || + context.Match("git://") || + context.Match("svn://") || + context.Match("ftp://") || + context.Match("mailto:")) { + // Handle most common URI schemes only + stringStyleBefore = context.state; + context.SetState(SCE_JSON_URI); + } else if (context.ch == '@') { + // https://www.w3.org/TR/json-ld/#dfn-keyword + if (IsNextWordInList(keywordsJSONLD, setKeywordJSONLD, context, styler)) { + stringStyleBefore = context.state; + context.SetState(SCE_JSON_LDKEYWORD); + } + } else { + compactIRI.checkChar(context.ch); + } + break; + case SCE_JSON_LDKEYWORD: + case SCE_JSON_URI: + if ((!setKeywordJSONLD.Contains(context.ch) && + (context.state == SCE_JSON_LDKEYWORD)) || + (!setURL.Contains(context.ch))) { + context.SetState(stringStyleBefore); + } + if (context.ch == '"') { + context.ForwardSetState(SCE_JSON_DEFAULT); + } else if (context.atLineEnd) { + context.ChangeState(SCE_JSON_STRINGEOL); + } + break; + case SCE_JSON_OPERATOR: + case SCE_JSON_NUMBER: + context.SetState(SCE_JSON_DEFAULT); + break; + case SCE_JSON_ERROR: + if (context.atLineEnd) { + context.SetState(SCE_JSON_DEFAULT); + } + break; + case SCE_JSON_KEYWORD: + if (!setKeywordJSON.Contains(context.ch)) { + context.SetState(SCE_JSON_DEFAULT); + } + break; + } + if (context.state == SCE_JSON_DEFAULT) { + if (context.ch == '"') { + compactIRI.resetState(); + context.SetState(SCE_JSON_STRING); + Sci_Position currPos = static_cast(context.currentPos); + if (AtPropertyName(styler, currPos)) { + context.SetState(SCE_JSON_PROPERTYNAME); + } + } else if (setOperators.Contains(context.ch)) { + context.SetState(SCE_JSON_OPERATOR); + } else if (options.allowComments && context.Match("/*")) { + context.SetState(SCE_JSON_BLOCKCOMMENT); + context.Forward(); + } else if (options.allowComments && context.Match("//")) { + context.SetState(SCE_JSON_LINECOMMENT); + } else if (setKeywordJSON.Contains(context.ch)) { + if (IsNextWordInList(keywordsJSON, setKeywordJSON, context, styler)) { + context.SetState(SCE_JSON_KEYWORD); + } + } + bool numberStart = + IsADigit(context.ch) && (context.chPrev == '+'|| + context.chPrev == '-' || + context.atLineStart || + IsASpace(context.chPrev) || + setOperators.Contains(context.chPrev)); + bool exponentPart = + tolower(context.ch) == 'e' && + IsADigit(context.chPrev) && + (IsADigit(context.chNext) || + context.chNext == '+' || + context.chNext == '-'); + bool signPart = + (context.ch == '-' || context.ch == '+') && + ((tolower(context.chPrev) == 'e' && IsADigit(context.chNext)) || + ((IsASpace(context.chPrev) || setOperators.Contains(context.chPrev)) + && IsADigit(context.chNext))); + bool adjacentDigit = + IsADigit(context.ch) && IsADigit(context.chPrev); + bool afterExponent = IsADigit(context.ch) && tolower(context.chPrev) == 'e'; + bool dotPart = context.ch == '.' && + IsADigit(context.chPrev) && + IsADigit(context.chNext); + bool afterDot = IsADigit(context.ch) && context.chPrev == '.'; + if (numberStart || + exponentPart || + signPart || + adjacentDigit || + dotPart || + afterExponent || + afterDot) { + context.SetState(SCE_JSON_NUMBER); + } else if (context.state == SCE_JSON_DEFAULT && !IsASpace(context.ch)) { + context.SetState(SCE_JSON_ERROR); + } + } + context.Forward(); + } + context.Complete(); +} + +void SCI_METHOD LexerJSON::Fold(Sci_PositionU startPos, + Sci_Position length, + int, + IDocument *pAccess) { + if (!options.fold) { + return; + } + LexAccessor styler(pAccess); + Sci_PositionU currLine = styler.GetLine(startPos); + Sci_PositionU endPos = startPos + length; + int currLevel = styler.LevelAt(currLine) & SC_FOLDLEVELNUMBERMASK; + int nextLevel = currLevel; + int visibleChars = 0; + for (Sci_PositionU i = startPos; i < endPos; i++) { + char curr = styler.SafeGetCharAt(i); + char next = styler.SafeGetCharAt(i+1); + bool atEOL = (curr == '\r' && next != '\n') || (curr == '\n'); + if (styler.StyleAt(i) == SCE_JSON_OPERATOR) { + if (curr == '{' || curr == '[') { + nextLevel++; + } else if (curr == '}' || curr == ']') { + nextLevel--; + } + } + if (atEOL || i == (endPos-1)) { + int level = currLevel; + if (!visibleChars && options.foldCompact) { + level |= SC_FOLDLEVELWHITEFLAG; + } else if (nextLevel > currLevel) { + level |= SC_FOLDLEVELHEADERFLAG; + } + if (level != styler.LevelAt(currLine)) { + styler.SetLevel(currLine, level); + } + currLine++; + currLevel = nextLevel; + visibleChars = 0; + } + if (!isspacechar(curr)) { + visibleChars++; + } + } +} + +LexerModule lmJSON(SCLEX_JSON, + LexerJSON::LexerFactoryJSON, + "json", + JSONWordListDesc); diff --git a/libs/qscintilla/src/Catalogue.cpp b/libs/qscintilla/src/Catalogue.cpp index 01c03550..375781d9 100644 --- a/libs/qscintilla/src/Catalogue.cpp +++ b/libs/qscintilla/src/Catalogue.cpp @@ -78,6 +78,7 @@ int Scintilla_LinkLexers() { //++Autogenerated -- run scripts/LexGen.py to regenerate //**\(\tLINK_LEXER(\*);\n\) LINK_LEXER(lmSQL); + LINK_LEXER(lmJSON); //--Autogenerated -- end of automatically generated section diff --git a/src/EditDialog.cpp b/src/EditDialog.cpp index aaf7b7f3..5659fe5b 100644 --- a/src/EditDialog.cpp +++ b/src/EditDialog.cpp @@ -31,11 +31,17 @@ EditDialog::EditDialog(QWidget* parent) hexLayout->addWidget(hexEdit); hexEdit->setOverwriteMode(false); + QHBoxLayout* jsonLayout = new QHBoxLayout(ui->editorJSON); + jsonEdit = new JsonTextEdit(this); + jsonLayout->addWidget(jsonEdit); + QShortcut* ins = new QShortcut(QKeySequence(Qt::Key_Insert), this); connect(ins, SIGNAL(activated()), this, SLOT(toggleOverwriteMode())); connect(ui->editorText, SIGNAL(textChanged()), this, SLOT(updateApplyButton())); connect(hexEdit, SIGNAL(dataChanged()), this, SLOT(updateApplyButton())); + connect(jsonEdit, SIGNAL(textChanged()), this, SLOT(updateApplyButton())); + connect(jsonEdit, SIGNAL(textChanged()), this, SLOT(editTextChanged())); reloadSettings(); } @@ -89,6 +95,9 @@ void EditDialog::loadData(const QByteArray& data) // Data type specific handling switch (dataType) { case Null: + // Set enabled any of the text widgets + ui->editorText->setEnabled(true); + jsonEdit->setEnabled(true); switch (editMode) { case TextEditor: // The text widget buffer is now the main data source @@ -96,10 +105,19 @@ void EditDialog::loadData(const QByteArray& data) // Empty the text editor contents, then enable text editing ui->editorText->clear(); - ui->editorText->setEnabled(true); break; + case JsonEditor: + // The JSON widget buffer is now the main data source + dataSource = JsonBuffer; + + // Empty the text editor contents, then enable text editing + jsonEdit->clear(); + + break; + + case HexEditor: // The hex widget buffer is now the main data source dataSource = HexBuffer; @@ -124,6 +142,10 @@ void EditDialog::loadData(const QByteArray& data) break; case Text: + // Set enabled any of the text widgets + ui->editorText->setEnabled(true); + jsonEdit->setEnabled(true); + switch (editMode) { case TextEditor: // The text widget buffer is now the main data source @@ -133,14 +155,24 @@ void EditDialog::loadData(const QByteArray& data) textData = QString::fromUtf8(data.constData(), data.size()); ui->editorText->setPlainText(textData); - // Enable text editing - ui->editorText->setEnabled(true); - // Select all of the text by default ui->editorText->selectAll(); break; + case JsonEditor: + // The JSON widget buffer is now the main data source + dataSource = JsonBuffer; + + // Load the text into the text editor + textData = QString::fromUtf8(data.constData(), data.size()); + jsonEdit->setText(textData); + + // Select all of the text by default + jsonEdit->selectAll(); + + break; + case HexEditor: // The hex widget buffer is now the main data source dataSource = HexBuffer; @@ -191,6 +223,12 @@ void EditDialog::loadData(const QByteArray& data) ui->editorText->setEnabled(false); break; + case JsonEditor: + // Disable text editing, and use a warning message as the contents + jsonEdit->setText(tr("Image data can't be viewed with the JSON editor")); + jsonEdit->setEnabled(false); + break; + case ImageViewer: // Load the image into the image viewing widget if (img.loadFromData(data)) { @@ -219,6 +257,12 @@ void EditDialog::loadData(const QByteArray& data) ui->editorText->setEnabled(false); break; + case JsonEditor: + // Disable text editing, and use a warning message as the contents + jsonEdit->setText(QString(tr("Binary data can't be viewed with the JSON editor"))); + jsonEdit->setEnabled(false); + break; + case ImageViewer: // Clear any image from the image viewing widget ui->editorImage->setPixmap(QPixmap(0,0)); @@ -281,12 +325,20 @@ void EditDialog::exportData() QFile file(fileName); if(file.open(QIODevice::WriteOnly)) { - if (dataSource == HexBuffer) { + switch (dataSource) { + case HexBuffer: // Data source is the hex buffer file.write(hexEdit->data()); - } else { + break; + case TextBuffer: // Data source is the text buffer file.write(ui->editorText->toPlainText().toUtf8()); + break; + case JsonBuffer: + // Data source is the JSON buffer + file.write(jsonEdit->text().toUtf8()); + break; + } file.close(); } @@ -298,16 +350,19 @@ void EditDialog::setNull() ui->editorText->clear(); ui->editorImage->clear(); hexEdit->setData(QByteArray()); + jsonEdit->clear(); dataType = Null; // Check if in text editor mode int editMode = ui->editorStack->currentIndex(); - if (editMode == TextEditor) { + if (editMode == TextEditor || editMode == JsonEditor) { // Setting NULL in the text editor switches the data source to it dataSource = TextBuffer; // Ensure the text editor is enabled ui->editorText->setEnabled(true); + // Ensure the JSON editor is enabled + jsonEdit->setEnabled(true); // The text editor doesn't know the difference between an empty string // and a NULL, so we need to record NULL outside of that @@ -331,7 +386,8 @@ void EditDialog::accept() if(!currentIndex.isValid()) return; - if (dataSource == TextBuffer) { + switch (dataSource) { + case TextBuffer: // Check if a NULL is set in the text editor if (textNullSet) { emit recordTextUpdated(currentIndex, hexEdit->data(), true); @@ -343,12 +399,28 @@ void EditDialog::accept() // The data is different, so commit it back to the database emit recordTextUpdated(currentIndex, newData.toUtf8(), false); } - } else { + break; + case JsonBuffer: + // Check if a NULL is set in the text editor + if (textNullSet) { + emit recordTextUpdated(currentIndex, hexEdit->data(), true); + } else { + // It's not NULL, so proceed with normal text string checking + QString oldData = currentIndex.data(Qt::EditRole).toString(); + QString newData = jsonEdit->text(); + if (oldData != newData) + // The data is different, so commit it back to the database + emit recordTextUpdated(currentIndex, newData.toUtf8(), false); + } + break; + + case HexBuffer: // The data source is the hex widget buffer, thus binary data QByteArray oldData = currentIndex.data(Qt::EditRole).toByteArray(); QByteArray newData = hexEdit->data(); if (newData != oldData) emit recordTextUpdated(currentIndex, newData, true); + break; } } @@ -356,12 +428,21 @@ void EditDialog::accept() void EditDialog::editModeChanged(int newMode) { // * If the dataSource is the text buffer, the data is always text * - if (dataSource == TextBuffer) { + switch (dataSource) { + case TextBuffer: switch (newMode) { case TextEditor: // Switching to the text editor // Nothing to do, as the text is already in the text buffer break; + case JsonEditor: // Switching to the JSON editor + // Convert the text widget buffer for the JSON widget + jsonEdit->setText(ui->editorText->toPlainText().toUtf8()); + + // The JSON widget buffer is now the main data source + dataSource = JsonBuffer; + break; + case HexEditor: // Switching to the hex editor // Convert the text widget buffer for the hex widget hexEdit->setData(ui->editorText->toPlainText().toUtf8()); @@ -379,24 +460,56 @@ void EditDialog::editModeChanged(int newMode) // Switch to the selected editor ui->editorStack->setCurrentIndex(newMode); return; - } + break; + case HexBuffer: - // * If the dataSource is the hex buffer, the contents could be anything - // so we just pass it to our loadData() function to handle * - if (dataSource == HexBuffer) { + // * If the dataSource is the hex buffer, the contents could be anything + // so we just pass it to our loadData() function to handle * // Switch to the selected editor first, as loadData() relies on it // being current ui->editorStack->setCurrentIndex(newMode); // Load the data into the appropriate widget, as done by loadData() loadData(hexEdit->data()); - } + break; + case JsonBuffer: + switch (newMode) { + case TextEditor: // Switching to the text editor + // Convert the text widget buffer for the JSON widget + ui->editorText->setText(jsonEdit->text()); + + // The Text widget buffer is now the main data source + dataSource = TextBuffer; + break; + + case JsonEditor: // Switching to the JSON editor + // Nothing to do, as the text is already in the JSON buffer + break; + + + case HexEditor: // Switching to the hex editor + // Convert the text widget buffer for the hex widget + hexEdit->setData(jsonEdit->text().toUtf8()); + + // The hex widget buffer is now the main data source + dataSource = HexBuffer; + break; + + case ImageViewer: + // Clear any image from the image viewing widget + ui->editorImage->setPixmap(QPixmap(0,0)); + break; + } + + // Switch to the selected editor + ui->editorStack->setCurrentIndex(newMode); + } } // Called for every keystroke in the text editor (only) void EditDialog::editTextChanged() { - if (dataSource == TextBuffer) { + if (dataSource == TextBuffer || dataSource == JsonBuffer) { // Data has been changed in the text editor, so it can't be a NULL // any more textNullSet = false; @@ -442,6 +555,7 @@ void EditDialog::toggleOverwriteMode() hexEdit->setOverwriteMode(currentMode); ui->editorText->setOverwriteMode(currentMode); + jsonEdit->setOverwriteMode(currentMode); } void EditDialog::setFocus() @@ -467,6 +581,7 @@ void EditDialog::setReadOnly(bool ro) ui->buttonImport->setEnabled(!ro); ui->editorText->setReadOnly(ro); ui->editorBinary->setEnabled(!ro); // We disable the entire hex editor here instead of setting it to read only because it doesn't have a setReadOnly() method + jsonEdit->setReadOnly(ro); } // Update the information labels in the bottom left corner of the dialog @@ -544,9 +659,16 @@ QString EditDialog::humanReadableSize(double byteCount) const void EditDialog::reloadSettings() { - // Set the font for the text and hex editors - QFont editorFont(Settings::getValue("databrowser", "font").toString()); - editorFont.setPointSize(Settings::getValue("databrowser", "fontsize").toInt()); - ui->editorText->setFont(editorFont); - hexEdit->setFont(editorFont); + // Set the databrowser font for the text editor but the (SQL) editor + // font for hex editor, since it needs a Monospace font and the + // databrowser font would be usually of variable width. + QFont textFont(Settings::getValue("databrowser", "font").toString()); + textFont.setPointSize(Settings::getValue("databrowser", "fontsize").toInt()); + ui->editorText->setFont(textFont); + + QFont hexFont(Settings::getValue("editor", "font").toString()); + hexFont.setPointSize(Settings::getValue("databrowser", "fontsize").toInt()); + hexEdit->setFont(hexFont); + + jsonEdit->reloadSettings(); } diff --git a/src/EditDialog.h b/src/EditDialog.h index 399be289..49802133 100644 --- a/src/EditDialog.h +++ b/src/EditDialog.h @@ -4,6 +4,8 @@ #include #include +#include "jsontextedit.h" + class QHexEdit; namespace Ui { @@ -47,6 +49,7 @@ signals: private: Ui::EditDialog* ui; QHexEdit* hexEdit; + JsonTextEdit* jsonEdit; QPersistentModelIndex currentIndex; int dataSource; int dataType; @@ -55,7 +58,8 @@ private: enum DataSources { TextBuffer, - HexBuffer + HexBuffer, + JsonBuffer }; enum DataTypes { @@ -68,7 +72,8 @@ private: enum EditModes { TextEditor = 0, HexEditor = 1, - ImageViewer = 2 + JsonEditor = 2, + ImageViewer = 3 }; int checkDataType(const QByteArray& data); diff --git a/src/EditDialog.ui b/src/EditDialog.ui index 97a1083d..474ddb33 100644 --- a/src/EditDialog.ui +++ b/src/EditDialog.ui @@ -47,6 +47,11 @@ Binary + + + JSON + + Image @@ -151,6 +156,7 @@ + true diff --git a/src/jsontextedit.cpp b/src/jsontextedit.cpp new file mode 100644 index 00000000..c7e2245a --- /dev/null +++ b/src/jsontextedit.cpp @@ -0,0 +1,150 @@ +#include "jsontextedit.h" +#include "Settings.h" + +#include +#include +#include +#include +#include +#include + +QsciLexerJSON* JsonTextEdit::jsonLexer = nullptr; + +JsonTextEdit::JsonTextEdit(QWidget* parent) : + QsciScintilla(parent) +{ + // Create lexer object if not done yet + if(jsonLexer == nullptr) + jsonLexer = new QsciLexerJSON(this); + + // Set the lexer + setLexer(jsonLexer); + + // Enable UTF8 + setUtf8(true); + + // Enable brace matching + setBraceMatching(QsciScintilla::SloppyBraceMatch); + + // Enable auto indentation + setAutoIndent(true); + + // Enable folding + setFolding(QsciScintilla::BoxedTreeFoldStyle); + jsonLexer->setFoldCompact(false); + + // Set a sensible scroll width, so the scroll bar is avoided in + // most cases. + setScrollWidth(80); + + // Scroll width is adjusted to ensure that all of the lines + // currently displayed can be completely scrolled. This mode never + // adjusts the scroll width to be narrower. + setScrollWidthTracking(true); + + // Do rest of initialisation + reloadSettings(); + + // Connect signals + connect(this, SIGNAL(linesChanged()), this, SLOT(updateLineNumberAreaWidth())); +} + +JsonTextEdit::~JsonTextEdit() +{ +} + +void JsonTextEdit::updateLineNumberAreaWidth() +{ + // Calculate number of digits of the current number of lines + int digits = std::floor(std::log10(lines())) + 1; + + // Calculate the width of this number if it was all zeros (this is because a 1 might require less space than a 0 and this could + // cause some flickering depending on the font) and set the new margin width. + QFont font = lexer()->font(QsciLexerJSON::Default); + setMarginWidth(0, QFontMetrics(font).width(QString("0").repeated(digits)) + 5); +} + +void JsonTextEdit::dropEvent(QDropEvent* e) +{ + QList urls = e->mimeData()->urls(); + if(urls.isEmpty()) + return QsciScintilla::dropEvent(e); + + QString file = urls.first().toLocalFile(); + if(!QFile::exists(file)) + return; + + QFile f(file); + f.open(QIODevice::ReadOnly); + setText(f.readAll()); + f.close(); +} + +void JsonTextEdit::setupSyntaxHighlightingFormat(const QString& settings_name, int style) +{ + jsonLexer->setColor(QColor(Settings::getValue("syntaxhighlighter", settings_name + "_colour").toString()), style); + + QFont font(Settings::getValue("editor", "font").toString()); + font.setPointSize(Settings::getValue("editor", "fontsize").toInt()); + font.setBold(Settings::getValue("syntaxhighlighter", settings_name + "_bold").toBool()); + font.setItalic(Settings::getValue("syntaxhighlighter", settings_name + "_italic").toBool()); + font.setUnderline(Settings::getValue("syntaxhighlighter", settings_name + "_underline").toBool()); + jsonLexer->setFont(font, style); +} + +void JsonTextEdit::reloadKeywords() +{ + // Set lexer again to reload the updated keywords list + setLexer(lexer()); +} + +void JsonTextEdit::reloadSettings() +{ + // Enable auto completion if it hasn't been disabled + if(Settings::getValue("editor", "auto_completion").toBool()) + { + setAutoCompletionThreshold(3); + setAutoCompletionCaseSensitivity(true); + setAutoCompletionShowSingle(true); + setAutoCompletionSource(QsciScintilla::AcsAPIs); + } else { + setAutoCompletionThreshold(0); + } + + // Set syntax highlighting settings + QFont defaultfont(Settings::getValue("editor", "font").toString()); + defaultfont.setStyleHint(QFont::TypeWriter); + defaultfont.setPointSize(Settings::getValue("editor", "fontsize").toInt()); + jsonLexer->setFont(defaultfont); + setupSyntaxHighlightingFormat("comment", QsciLexerJSON::CommentLine); + setupSyntaxHighlightingFormat("comment", QsciLexerJSON::CommentBlock); + setupSyntaxHighlightingFormat("keyword", QsciLexerJSON::Keyword); + setupSyntaxHighlightingFormat("keyword", QsciLexerJSON::KeywordLD); + setupSyntaxHighlightingFormat("string", QsciLexerJSON::String); + setupSyntaxHighlightingFormat("table", QsciLexerJSON::Number); + setupSyntaxHighlightingFormat("identifier", QsciLexerJSON::Property); + jsonLexer->setHighlightComments(true); + + // Set font + QFont font(Settings::getValue("editor", "font").toString()); + font.setStyleHint(QFont::TypeWriter); + font.setPointSize(Settings::getValue("editor", "fontsize").toInt()); + setFont(font); + + // Show line numbers + QFont marginsfont(QFont(Settings::getValue("editor", "font").toString())); + marginsfont.setPointSize(font.pointSize()); + setMarginsFont(marginsfont); + setMarginLineNumbers(0, true); + setMarginsBackgroundColor(Qt::lightGray); + updateLineNumberAreaWidth(); + + // Highlight current line + setCaretLineVisible(true); + setCaretLineBackgroundColor(QColor(Settings::getValue("syntaxhighlighter", "currentline_colour").toString())); + + // Set tab width + setTabWidth(Settings::getValue("editor", "tabsize").toInt()); + jsonLexer->refreshProperties(); + +} diff --git a/src/jsontextedit.h b/src/jsontextedit.h new file mode 100644 index 00000000..8669cf96 --- /dev/null +++ b/src/jsontextedit.h @@ -0,0 +1,35 @@ +#ifndef JSONTEXTEDIT_H +#define JSONTEXTEDIT_H + +#include "Qsci/qsciscintilla.h" +#include + +/** + * @brief The JsonTextEdit class + * This class is based on the QScintilla widget + */ +class JsonTextEdit : public QsciScintilla +{ + Q_OBJECT + +public: + explicit JsonTextEdit(QWidget *parent = nullptr); + virtual ~JsonTextEdit(); + + static QsciLexerJSON* jsonLexer; + +public slots: + void reloadKeywords(); + void reloadSettings(); + +protected: + void dropEvent(QDropEvent* e); + +private: + void setupSyntaxHighlightingFormat(const QString& settings_name, int style); + +private slots: + void updateLineNumberAreaWidth(); +}; + +#endif