1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "syntaxhighlighter.h"
5#include "syntaxhighlighterdata.h"
6
7#include <QtCore/qregularexpression.h>
8
9#ifdef QQEM_SYNTAX_HIGHLIGHTING_ENABLED
10#include <QtQuick3DGlslParser/private/glsllexer_p.h>
11#include <QtQuick3DGlslParser/private/glslparser_p.h>
12#endif
13
14// Text color and style categories
15enum TextStyle : quint8 {
16 C_VISUAL_WHITESPACE,
17 C_PREPROCESSOR,
18 C_NUMBER,
19 C_COMMENT,
20 C_KEYWORD,
21 C_PARAMETER,
22 C_REMOVED_LINE,
23 C_TAG
24};
25
26static constexpr TextStyle GLSLReservedKeyword = C_REMOVED_LINE;
27
28static QTextCharFormat formatForCategory(TextStyle category)
29{
30 switch (category) {
31 case C_PARAMETER:
32 {
33 QTextCharFormat fmt;
34 fmt.setForeground(QColor::fromRgb(r: 0xaa, g: 0xaa, b: 0xff));
35 return fmt;
36 }
37 case C_PREPROCESSOR:
38 {
39 QTextCharFormat fmt;
40 fmt.setForeground(QColor::fromRgb(r: 0x55, g: 0x55, b: 0xff));
41 return fmt;
42 }
43 case C_NUMBER:
44 {
45 QTextCharFormat fmt;
46 fmt.setForeground(QColor::fromRgb(r: 0xff, g: 0x55, b: 0xff));
47 return fmt;
48 }
49 case C_COMMENT:
50 {
51 QTextCharFormat fmt;
52 fmt.setForeground(QColor::fromRgb(r: 0x80, g: 0x80, b: 0x80));
53 return fmt;
54 }
55 case C_KEYWORD:
56 {
57 QTextCharFormat fmt;
58 fmt.setForeground(QColor::fromRgb(r: 0xff, g: 0xff, b: 0x55));
59 return fmt;
60 }
61 case C_TAG:
62 {
63 QTextCharFormat fmt;
64 fmt.setForeground(QColor::fromRgb(r: 0xaa, g: 0xff, b: 0xaa));
65 return fmt;
66 }
67 case C_REMOVED_LINE:
68 Q_FALLTHROUGH();
69 case C_VISUAL_WHITESPACE:
70 break;
71 }
72
73 return QTextCharFormat();
74}
75
76SyntaxHighlighter::SyntaxHighlighter(QObject *p)
77 : QSyntaxHighlighter(p)
78{
79
80}
81
82#ifdef QQEM_SYNTAX_HIGHLIGHTING_ENABLED
83void SyntaxHighlighter::highlightBlock(const QString &text)
84{
85 if (m_tagKeywords.isEmpty()) {
86 const auto tagKeywords = SyntaxHighlighterData::reservedTagNames();
87 for (const auto &kw : tagKeywords)
88 m_tagKeywords.insert(value: kw);
89 }
90 if (m_argumentKeywords.isEmpty()) {
91 const auto args = SyntaxHighlighterData::reservedArgumentNames();
92 for (const auto &arg : args)
93 m_argumentKeywords.insert(value: arg);
94 }
95 const int previousState = previousBlockState();
96 int state = 0;
97 if (previousState != -1)
98 state = previousState & 0xff;
99
100 const QByteArray data = text.toLatin1();
101 GLSL::Lexer lex(/*engine=*/ nullptr, data.constData(), data.size());
102 lex.setState(state);
103 lex.setScanKeywords(false);
104 lex.setScanComments(true);
105
106 lex.setVariant(GLSL::Lexer::Variant_GLSL_400);
107
108 QList<GLSL::Token> tokens;
109 GLSL::Token tk;
110 do {
111 lex.yylex(tk: &tk);
112 tokens.append(t: tk);
113 } while (tk.isNot(k: GLSL::Parser::EOF_SYMBOL));
114
115 state = lex.state(); // refresh the state
116
117 if (tokens.isEmpty()) {
118 setCurrentBlockState(previousState);
119 if (!text.isEmpty()) // the empty line can still contain whitespace
120 setFormat(start: 0, count: text.length(), format: formatForCategory(category: C_VISUAL_WHITESPACE));
121 return;
122 }
123
124 for (int i = 0, end = tokens.size(); i != end; ++i) {
125 const GLSL::Token &tk = tokens.at(i);
126
127 int previousTokenEnd = 0;
128 if (i != 0) {
129 // mark the whitespaces
130 previousTokenEnd = tokens.at(i: i - 1).begin() + tokens.at(i: i - 1).length;
131 }
132
133 if (previousTokenEnd != tk.begin())
134 setFormat(start: previousTokenEnd, count: tk.begin() - previousTokenEnd, format: formatForCategory(category: C_VISUAL_WHITESPACE));
135
136 if (tk.is(k: GLSL::Parser::T_NUMBER)) {
137 setFormat(start: tk.begin(), count: tk.length, format: formatForCategory(category: C_NUMBER));
138
139 } else if (tk.is(k: GLSL::Parser::T_IDENTIFIER)) {
140 int kind = lex.findKeyword(word: data.constData() + tk.position, length: tk.length);
141 if (kind == GLSL::Parser::T_IDENTIFIER) {
142 if (m_argumentKeywords.contains(value: QByteArrayView(data.constData() + tk.position, tk.length)))
143 setFormat(start: tk.position, count: tk.length, format: formatForCategory(category: C_PARAMETER));
144 }
145 if (kind == GLSL::Parser::T_RESERVED) {
146 setFormat(start: tk.position, count: tk.length, format: formatForCategory(category: GLSLReservedKeyword));
147 } else if (kind != GLSL::Parser::T_IDENTIFIER) {
148 setFormat(start: tk.position, count: tk.length, format: formatForCategory(category: C_KEYWORD));
149 } else {
150 const auto tagStartPos = tk.position - 1;
151 bool isTag = data.at(i: tagStartPos) == '@';
152 if (isTag) {
153 const auto tagLength = tk.length + 1;
154 auto line = QByteArrayView(data.constData() + tagStartPos, tagLength).trimmed();
155 auto firstSpace = line.indexOf(ch: ' ');
156 QByteArrayView firstWord;
157 if (firstSpace > 0)
158 firstWord = line.sliced(pos: 0, n: firstSpace);
159 if (m_tagKeywords.contains(value: line) || (!firstWord.isEmpty() && m_tagKeywords.contains(value: firstWord)))
160 setFormat(start: tagStartPos, count: tagLength, format: formatForCategory(category: C_TAG));
161 }
162 }
163 } else if (tk.is(k: GLSL::Parser::T_COMMENT)) {
164 highlightLine(text, position: tk.begin(), length: tk.length, format: formatForCategory(category: C_COMMENT));
165 }
166 }
167
168 // mark the trailing white spaces
169 {
170 const GLSL::Token tk = tokens.last();
171 const int lastTokenEnd = tk.begin() + tk.length;
172 if (text.length() > lastTokenEnd)
173 highlightLine(text, position: lastTokenEnd, length: text.length() - lastTokenEnd, format: QTextCharFormat());
174 }
175
176 // Multi-line comments
177 static QRegularExpression startExpression("/\\*");
178 static QRegularExpression endExpression("\\*/");
179
180 setCurrentBlockState(None);
181
182 int startIndex = 0;
183 if (previousBlockState() != Comment)
184 startIndex = text.indexOf(re: startExpression);
185
186 // Check multi-line comment blocks
187 while (startIndex >= 0) {
188 QRegularExpressionMatch endMatch;
189 int endIndex = text.indexOf(re: endExpression, from: startIndex, rmatch: &endMatch);
190 int commentLength;
191 if (endIndex == -1) {
192 setCurrentBlockState(Comment);
193 commentLength = text.length() - startIndex;
194 } else {
195 commentLength = endIndex - startIndex
196 + endMatch.capturedLength();
197 }
198 setFormat(start: startIndex, count: commentLength, format: formatForCategory(category: C_COMMENT));
199 startIndex = text.indexOf(re: startExpression,
200 from: startIndex + commentLength);
201 }
202}
203
204#else
205
206// Dummy version without glslparser dependency
207void SyntaxHighlighter::highlightBlock(const QString &text)
208{
209 Q_UNUSED(text)
210}
211
212#endif
213
214void SyntaxHighlighter::highlightLine(const QString &text, int position, int length,
215 const QTextCharFormat &format)
216{
217 const QTextCharFormat visualSpaceFormat = formatForCategory(category: C_VISUAL_WHITESPACE);
218
219 const int end = position + length;
220 int index = position;
221
222 while (index != end) {
223 const bool isSpace = text.at(i: index).isSpace();
224 const int start = index;
225
226 do {
227 ++index;
228 } while (index != end && text.at(i: index).isSpace() == isSpace);
229
230 const int tokenLength = index - start;
231 if (isSpace)
232 setFormat(start, count: tokenLength, format: visualSpaceFormat);
233 else if (format.isValid())
234 setFormat(start, count: tokenLength, format);
235 }
236}
237
238QQuickTextDocument *SyntaxHighlighter::document() const
239{
240 return m_quickTextDocument;
241}
242
243void SyntaxHighlighter::setDocument(QQuickTextDocument *newDocument)
244{
245 if (m_quickTextDocument == newDocument)
246 return;
247
248 m_quickTextDocument = newDocument;
249 QSyntaxHighlighter::setDocument(m_quickTextDocument != nullptr ? m_quickTextDocument->textDocument() : nullptr);
250
251 emit documentChanged();
252}
253

source code of qtquickeffectmaker/tools/qqem/syntaxhighlighter.cpp