// Copyright (C) 2025 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "syntaxhighlighter.h" #include #include #include #include #include QT_BEGIN_NAMESPACE // Text color and style categories enum TextStyle : quint8 { C_VISUAL_WHITESPACE, C_PREPROCESSOR, C_NUMBER, C_COMMENT, C_KEYWORD, C_PARAMETER, C_REMOVED_LINE }; static constexpr TextStyle GLSLReservedKeyword = C_REMOVED_LINE; static QTextCharFormat formatForCategory(TextStyle category) { switch (category) { case C_PARAMETER: { QTextCharFormat fmt; fmt.setForeground(QColor::fromRgb(0xaa, 0xaa, 0xff)); return fmt; } case C_PREPROCESSOR: { QTextCharFormat fmt; fmt.setForeground(QColor::fromRgb(0x55, 0x55, 0xff)); return fmt; } case C_NUMBER: { QTextCharFormat fmt; fmt.setForeground(QColor::fromRgb(0xff, 0x55, 0xff)); return fmt; } case C_COMMENT: { QTextCharFormat fmt; fmt.setForeground(QColor::fromRgb(0x55, 0xff, 0xff)); return fmt; } case C_KEYWORD: { QTextCharFormat fmt; fmt.setForeground(QColor::fromRgb(0xff, 0xff, 0x55)); return fmt; } case C_REMOVED_LINE: Q_FALLTHROUGH(); case C_VISUAL_WHITESPACE: break; } return QTextCharFormat(); } SyntaxHighlighter::SyntaxHighlighter(QObject *p) : QSyntaxHighlighter(p) { } void SyntaxHighlighter::highlightBlock(const QString &text) { if (m_keywords.isEmpty()) { const auto keywords = QtQuick3DEditorHelpers::CustomMaterial::preprocessorVars(); for (const auto &kw : keywords) m_keywords.insert(kw); } if (m_argumentKeywords.isEmpty()) { const auto args = QtQuick3DEditorHelpers::CustomMaterial::reservedArgumentNames(); for (const auto &arg : args) m_argumentKeywords.insert(arg); } const int previousState = previousBlockState(); int state = 0; if (previousState != -1) state = previousState & 0xff; const QByteArray data = text.toLatin1(); GLSL::Lexer lex(/*engine=*/ nullptr, data.constData(), data.size()); lex.setState(state); lex.setScanKeywords(false); lex.setScanComments(true); lex.setVariant(GLSL::Lexer::Variant_GLSL_400); QList tokens; GLSL::Token tk; do { lex.yylex(&tk); tokens.append(tk); } while (tk.isNot(GLSL::Parser::EOF_SYMBOL)); state = lex.state(); // refresh the state if (tokens.isEmpty()) { setCurrentBlockState(previousState); if (!text.isEmpty()) // the empty line can still contain whitespace setFormat(0, text.size(), formatForCategory(C_VISUAL_WHITESPACE)); return; } for (int i = 0, end = tokens.size(); i != end; ++i) { const GLSL::Token &tk = tokens.at(i); int previousTokenEnd = 0; if (i != 0) { // mark the whitespaces previousTokenEnd = tokens.at(i - 1).begin() + tokens.at(i - 1).length; } if (previousTokenEnd != tk.begin()) setFormat(previousTokenEnd, tk.begin() - previousTokenEnd, formatForCategory(C_VISUAL_WHITESPACE)); if (tk.is(GLSL::Parser::T_NUMBER)) { setFormat(tk.begin(), tk.length, formatForCategory(C_NUMBER)); } else if (tk.is(GLSL::Parser::T_COMMENT)) { highlightLine(text, tk.begin(), tk.length, formatForCategory(C_COMMENT)); } else if (tk.is(GLSL::Parser::T_IDENTIFIER)) { int kind = lex.findKeyword(data.constData() + tk.position, tk.length); if (kind == GLSL::Parser::T_IDENTIFIER) { if (m_keywords.contains(QByteArrayView(data.constData() + tk.position, tk.length))) setFormat(tk.position, tk.length, formatForCategory(C_PREPROCESSOR)); if (m_argumentKeywords.contains(QByteArrayView(data.constData() + tk.position, tk.length))) setFormat(tk.position, tk.length, formatForCategory(C_PARAMETER)); } if (kind == GLSL::Parser::T_RESERVED) setFormat(tk.position, tk.length, formatForCategory(GLSLReservedKeyword)); else if (kind != GLSL::Parser::T_IDENTIFIER) setFormat(tk.position, tk.length, formatForCategory(C_KEYWORD)); } } // mark the trailing white spaces { const GLSL::Token tk = tokens.last(); const int lastTokenEnd = tk.begin() + tk.length; if (text.size() > lastTokenEnd) highlightLine(text, lastTokenEnd, text.size() - lastTokenEnd, QTextCharFormat()); } } void SyntaxHighlighter::highlightLine(const QString &text, int position, int length, const QTextCharFormat &format) { const QTextCharFormat visualSpaceFormat = formatForCategory(C_VISUAL_WHITESPACE); const int end = position + length; int index = position; while (index != end) { const bool isSpace = text.at(index).isSpace(); const int start = index; do { ++index; } while (index != end && text.at(index).isSpace() == isSpace); const int tokenLength = index - start; if (isSpace) setFormat(start, tokenLength, visualSpaceFormat); else if (format.isValid()) setFormat(start, tokenLength, format); } } QQuickTextDocument *SyntaxHighlighter::document() const { return m_quickTextDocument; } void SyntaxHighlighter::setDocument(QQuickTextDocument *newDocument) { if (m_quickTextDocument == newDocument) return; m_quickTextDocument = newDocument; QSyntaxHighlighter::setDocument(m_quickTextDocument != nullptr ? m_quickTextDocument->textDocument() : nullptr); emit documentChanged(); } QT_END_NAMESPACE