ACAV f0ba4b7c9529
Abstract Syntax Tree (AST) visualization tool for C, C++, and Objective-C
Loading...
Searching...
No Matches
CppSyntaxHighlighter.cpp
1/*$!{
2* Aurora Clang AST Viewer (ACAV)
3*
4* Copyright (c) 2026 Min Liu
5* Copyright (c) 2026 Michael David Adams
6*
7* SPDX-License-Identifier: GPL-2.0-or-later
8*
9* This program is free software; you can redistribute it and/or modify
10* it under the terms of the GNU General Public License as published by
11* the Free Software Foundation; either version 2 of the License, or
12* (at your option) any later version.
13*
14* This program is distributed in the hope that it will be useful,
15* but WITHOUT ANY WARRANTY; without even the implied warranty of
16* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17* GNU General Public License for more details.
18*
19* You should have received a copy of the GNU General Public License along
20* with this program; if not, see <https://www.gnu.org/licenses/>.
21}$!*/
22
24#include <QColor>
25#include <QStringList>
26
27namespace acav {
28
29CppSyntaxHighlighter::CppSyntaxHighlighter(QTextDocument *parent)
30 : QSyntaxHighlighter(parent),
31 functionPattern_(
32 QStringLiteral("\\b([A-Za-z_][A-Za-z0-9_]*)\\s*(?=\\()")) {
33 commentFormat_.setForeground(QColor(110, 118, 129));
34 commentFormat_.setFontItalic(true);
35
36 stringFormat_.setForeground(QColor(10, 122, 76));
37
38 keywordFormat_.setForeground(QColor(20, 80, 160));
39 keywordFormat_.setFontWeight(QFont::Bold);
40
41 functionFormat_.setForeground(QColor(146, 64, 14));
42 functionFormat_.setFontUnderline(true);
43
44 preprocessorFormat_.setForeground(QColor(130, 60, 130));
45
46 static const QStringList keywords = {
47 QStringLiteral("alignas"), QStringLiteral("alignof"),
48 QStringLiteral("asm"), QStringLiteral("auto"),
49 QStringLiteral("bool"), QStringLiteral("break"),
50 QStringLiteral("case"), QStringLiteral("catch"),
51 QStringLiteral("char"), QStringLiteral("class"),
52 QStringLiteral("const"), QStringLiteral("constexpr"),
53 QStringLiteral("consteval"), QStringLiteral("constinit"),
54 QStringLiteral("continue"), QStringLiteral("decltype"),
55 QStringLiteral("default"), QStringLiteral("delete"),
56 QStringLiteral("do"), QStringLiteral("double"),
57 QStringLiteral("else"), QStringLiteral("enum"),
58 QStringLiteral("explicit"), QStringLiteral("extern"),
59 QStringLiteral("false"), QStringLiteral("float"),
60 QStringLiteral("for"), QStringLiteral("friend"),
61 QStringLiteral("goto"), QStringLiteral("if"),
62 QStringLiteral("inline"), QStringLiteral("int"),
63 QStringLiteral("long"), QStringLiteral("mutable"),
64 QStringLiteral("namespace"), QStringLiteral("new"),
65 QStringLiteral("noexcept"), QStringLiteral("nullptr"),
66 QStringLiteral("operator"), QStringLiteral("private"),
67 QStringLiteral("protected"), QStringLiteral("public"),
68 QStringLiteral("register"), QStringLiteral("reinterpret_cast"),
69 QStringLiteral("requires"), QStringLiteral("return"),
70 QStringLiteral("short"), QStringLiteral("signed"),
71 QStringLiteral("sizeof"), QStringLiteral("static"),
72 QStringLiteral("static_assert"), QStringLiteral("struct"),
73 QStringLiteral("switch"), QStringLiteral("template"),
74 QStringLiteral("this"), QStringLiteral("thread_local"),
75 QStringLiteral("throw"), QStringLiteral("true"),
76 QStringLiteral("try"), QStringLiteral("typedef"),
77 QStringLiteral("typename"), QStringLiteral("union"),
78 QStringLiteral("unsigned"), QStringLiteral("using"),
79 QStringLiteral("virtual"), QStringLiteral("void"),
80 QStringLiteral("volatile"), QStringLiteral("while")};
81
82 keywordPatterns_.reserve(static_cast<std::size_t>(keywords.size()));
83 for (const QString &keyword : keywords) {
84 keywordPatterns_.push_back(
85 QRegularExpression(QStringLiteral("\\b%1\\b").arg(keyword)));
86 }
87
88 nonFunctionWords_ = QSet<QString>(
89 keywords.begin(), keywords.end()); // Prevent keywords from function style
90}
91
92void CppSyntaxHighlighter::highlightBlock(const QString &text) {
93 std::vector<bool> masked(static_cast<std::size_t>(text.size()), false);
94 int index = 0;
95 int state = previousBlockState();
96 setCurrentBlockState(NormalState);
97
98 if (state == InBlockCommentState) {
99 int end = text.indexOf(QStringLiteral("*/"));
100 if (end < 0) {
101 markCommentRange(text, 0, text.size(), &masked);
102 setCurrentBlockState(InBlockCommentState);
103 return;
104 }
105 markCommentRange(text, 0, end + 2, &masked);
106 index = end + 2;
107 }
108
109 while (index < text.size()) {
110 const QChar ch = text.at(index);
111
112 if (ch == '"' || ch == '\'') {
113 int nextIndex = index + 1;
114 markStringOrCharLiteral(text, index, &masked, &nextIndex);
115 index = nextIndex;
116 continue;
117 }
118
119 if (ch == '/' && index + 1 < text.size()) {
120 const QChar next = text.at(index + 1);
121 if (next == '/') {
122 markCommentRange(text, index, text.size() - index, &masked);
123 break;
124 }
125 if (next == '*') {
126 int end = text.indexOf(QStringLiteral("*/"), index + 2);
127 if (end < 0) {
128 markCommentRange(text, index, text.size() - index, &masked);
129 setCurrentBlockState(InBlockCommentState);
130 break;
131 }
132 markCommentRange(text, index, end - index + 2, &masked);
133 index = end + 2;
134 continue;
135 }
136 }
137
138 ++index;
139 }
140
141 applyKeywordRules(text, masked);
142 applyFunctionRules(text, masked);
143 applyPreprocessorRules(text, &masked);
144}
145
146void CppSyntaxHighlighter::markCommentRange(const QString &text, int start,
147 int length,
148 std::vector<bool> *masked) {
149 if (!masked || start < 0 || length <= 0 || start >= text.size()) {
150 return;
151 }
152
153 int safeLength = qMin(length, text.size() - start);
154 setFormat(start, safeLength, commentFormat_);
155 for (int i = start; i < start + safeLength; ++i) {
156 (*masked)[static_cast<std::size_t>(i)] = true;
157 }
158}
159
160void CppSyntaxHighlighter::markStringOrCharLiteral(const QString &text,
161 int start,
162 std::vector<bool> *masked,
163 int *nextIndex) {
164 if (!masked || !nextIndex || start < 0 || start >= text.size()) {
165 return;
166 }
167
168 const QChar quote = text.at(start);
169 int end = start + 1;
170 bool escaped = false;
171
172 while (end < text.size()) {
173 const QChar ch = text.at(end);
174 if (escaped) {
175 escaped = false;
176 } else if (ch == '\\') {
177 escaped = true;
178 } else if (ch == quote) {
179 ++end;
180 break;
181 }
182 ++end;
183 }
184
185 int length = end - start;
186 if (length <= 0) {
187 *nextIndex = start + 1;
188 return;
189 }
190
191 setFormat(start, length, stringFormat_);
192 for (int i = start; i < start + length && i < text.size(); ++i) {
193 (*masked)[static_cast<std::size_t>(i)] = true;
194 }
195 *nextIndex = end;
196}
197
198bool CppSyntaxHighlighter::isUnmaskedRange(const std::vector<bool> &masked,
199 int start, int length) const {
200 if (start < 0 || length <= 0) {
201 return false;
202 }
203 for (int i = start; i < start + length; ++i) {
204 if (i >= static_cast<int>(masked.size()) ||
205 masked[static_cast<std::size_t>(i)]) {
206 return false;
207 }
208 }
209 return true;
210}
211
212void CppSyntaxHighlighter::applyKeywordRules(const QString &text,
213 const std::vector<bool> &masked) {
214 for (const QRegularExpression &pattern : keywordPatterns_) {
215 auto it = pattern.globalMatch(text);
216 while (it.hasNext()) {
217 const auto match = it.next();
218 const int start = match.capturedStart();
219 const int length = match.capturedLength();
220 if (isUnmaskedRange(masked, start, length)) {
221 setFormat(start, length, keywordFormat_);
222 }
223 }
224 }
225}
226
227void CppSyntaxHighlighter::applyFunctionRules(const QString &text,
228 const std::vector<bool> &masked) {
229 auto it = functionPattern_.globalMatch(text);
230 while (it.hasNext()) {
231 const auto match = it.next();
232 const QString identifier = match.captured(1);
233 if (identifier.isEmpty() || nonFunctionWords_.contains(identifier)) {
234 continue;
235 }
236 const int start = match.capturedStart(1);
237 const int length = match.capturedLength(1);
238 if (isUnmaskedRange(masked, start, length)) {
239 setFormat(start, length, functionFormat_);
240 }
241 }
242}
243
244void CppSyntaxHighlighter::applyPreprocessorRules(const QString &text,
245 std::vector<bool> *masked) {
246 if (!masked) {
247 return;
248 }
249
250 // Find first non-whitespace character
251 int start = 0;
252 while (start < text.size() && text.at(start).isSpace()) {
253 ++start;
254 }
255
256 // Must start with '#'
257 if (start >= text.size() || text.at(start) != '#') {
258 return;
259 }
260
261 // Find the directive word after '#'
262 int directiveStart = start + 1;
263 while (directiveStart < text.size() && text.at(directiveStart).isSpace()) {
264 ++directiveStart;
265 }
266 int directiveEnd = directiveStart;
267 while (directiveEnd < text.size() && text.at(directiveEnd).isLetterOrNumber()) {
268 ++directiveEnd;
269 }
270
271 // Highlight '#directive' with preprocessor format (only unmasked parts)
272 if (directiveEnd > start) {
273 for (int i = start; i < directiveEnd; ++i) {
274 if (!(*masked)[static_cast<std::size_t>(i)]) {
275 setFormat(i, 1, preprocessorFormat_);
276 }
277 }
278 }
279
280 // For #include, also highlight <...> as a string
281 const QStringView directive =
282 QStringView(text).mid(directiveStart, directiveEnd - directiveStart);
283 if (directive == QStringLiteral("include")) {
284 int pos = directiveEnd;
285 while (pos < text.size() && text.at(pos).isSpace()) {
286 ++pos;
287 }
288 if (pos < text.size() && text.at(pos) == '<') {
289 int angleEnd = text.indexOf('>', pos + 1);
290 if (angleEnd >= 0) {
291 int length = angleEnd - pos + 1;
292 setFormat(pos, length, stringFormat_);
293 for (int i = pos; i < pos + length; ++i) {
294 (*masked)[static_cast<std::size_t>(i)] = true;
295 }
296 }
297 }
298 }
299}
300
301} // namespace acav
Lightweight C/C++ syntax highlighter for SourceCodeView.