1 // This file is part of the SpeedCrunch project
2 // Copyright (C) 2007 Ariya Hidayat <ariya@kde.org>
3 // Copyright (C) 2007-2016 @heldercorreia
4 // Copyright (c) 2013 Larswad
5 //
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License
8 // as published by the Free Software Foundation; either version 2
9 // of the License, or (at your option) any later version.
10 //
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
15 //
16 // You should have received a copy of the GNU General Public License
17 // along with this program; see the file COPYING. If not, write to
18 // the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 // Boston, MA 02110-1301, USA.
20 //
21
22 #include "gui/syntaxhighlighter.h"
23
24 #include "core/evaluator.h"
25 #include "core/functions.h"
26 #include "core/settings.h"
27
28 #include <QtCore/QDir>
29 #include <QtCore/QJsonObject>
30 #include <QLatin1String>
31 #include <QApplication>
32 #include <QPalette>
33 #include <QPlainTextEdit>
34 #include <QTextDocument>
35 #include <QTextDocumentFragment>
36
colorSchemeSearchPaths()37 static const QVector<QString> colorSchemeSearchPaths()
38 {
39 static QVector<QString> searchPaths;
40 if (searchPaths.isEmpty()) {
41 // By only populating the paths in a function when they're used, we ensure all the QApplication
42 // fields that are used by QStandardPaths are set.
43 searchPaths.append(QString("%1/color-schemes").arg(Settings::getDataPath()));
44 searchPaths.append(QStringLiteral(":/color-schemes"));
45 }
46 return searchPaths;
47 }
48
getFallbackColor(ColorScheme::Role role)49 QColor getFallbackColor(ColorScheme::Role role)
50 {
51 switch (role) {
52 case ColorScheme::Background:
53 case ColorScheme::EditorBackground:
54 return QApplication::palette().color(QPalette::Base);
55 default:
56 return QApplication::palette().color(QPalette::Text);
57 }
58 }
59
ColorScheme(const QJsonDocument & doc)60 ColorScheme::ColorScheme(const QJsonDocument& doc)
61 : m_valid(false)
62 {
63 static const QVector<std::pair<QString, ColorScheme::Role>> RoleNames {
64 { QStringLiteral("cursor"), ColorScheme::Cursor },
65 { QStringLiteral("number"), ColorScheme::Number },
66 { QStringLiteral("parens"), ColorScheme::Parens },
67 { QStringLiteral("result"), ColorScheme::Result },
68 { QStringLiteral("comment"), ColorScheme::Comment },
69 { QStringLiteral("matched"), ColorScheme::Matched },
70 { QStringLiteral("function"), ColorScheme::Function },
71 { QStringLiteral("operator"), ColorScheme::Operator },
72 { QStringLiteral("variable"), ColorScheme::Variable },
73 { QStringLiteral("scrollbar"), ColorScheme::ScrollBar },
74 { QStringLiteral("separator"), ColorScheme::Separator },
75 { QStringLiteral("background"), ColorScheme::Background },
76 { QStringLiteral("editorbackground"), ColorScheme::EditorBackground },
77 };
78 if (!doc.isObject())
79 return;
80 auto obj = doc.object();
81 for (auto& role : RoleNames) {
82 auto v = obj.value(role.first);
83 if (v.isUndefined())
84 // Having a key missing is fine...
85 continue;
86 auto color = QColor(v.toString());
87 if (!color.isValid())
88 // ...having one that's not a color is not.
89 return;
90 m_colors.insert(role.second, color);
91 }
92 m_valid = true;
93 }
94
colorForRole(Role role) const95 QColor ColorScheme::colorForRole(Role role) const
96 {
97 QColor color = m_colors[role];
98 if (!color.isValid())
99 return getFallbackColor(role);
100 else
101 return color;
102 }
103
enumerate()104 QStringList ColorScheme::enumerate()
105 {
106 QMap<QString, void*> colorSchemes;
107 for (auto& searchPath : colorSchemeSearchPaths()) {
108 QDir dir(searchPath);
109 dir.setFilter(QDir::Files | QDir::Readable);
110 dir.setNameFilters({ QString("*.%1").arg(m_colorSchemeExtension) });
111 const auto infoList = dir.entryInfoList();
112 for (auto& info : infoList) // TODO: Use Qt 5.7's qAsConst().
113 colorSchemes.insert(info.completeBaseName(), nullptr);
114 }
115 // Since this is a QMap, the keys are already sorted in ascending order.
116 return colorSchemes.keys();
117 }
118
loadFromFile(const QString & path)119 ColorScheme ColorScheme::loadFromFile(const QString& path)
120 {
121 QFile file(path);
122 if (!file.open(QIODevice::ReadOnly))
123 return ColorScheme();
124 // TODO: Better error handling.
125 return ColorScheme(QJsonDocument::fromJson(file.readAll()));
126 }
127
loadByName(const QString & name)128 ColorScheme ColorScheme::loadByName(const QString& name)
129 {
130 for (auto& path : colorSchemeSearchPaths()) {
131 auto fileName = QString("%1/%2.%3").arg(path).arg(name).arg(m_colorSchemeExtension);
132 auto colorScheme = loadFromFile(fileName);
133 if (colorScheme.isValid())
134 return colorScheme;
135 }
136 return ColorScheme();
137 }
138
139
SyntaxHighlighter(QPlainTextEdit * edit)140 SyntaxHighlighter::SyntaxHighlighter(QPlainTextEdit* edit)
141 : QSyntaxHighlighter(edit)
142 {
143 setDocument(edit->document());
144 update();
145 }
146
setColorScheme(ColorScheme && colorScheme)147 void SyntaxHighlighter::setColorScheme(ColorScheme&& colorScheme) {
148 m_colorScheme = colorScheme;
149 }
150
highlightBlock(const QString & text)151 void SyntaxHighlighter::highlightBlock(const QString& text)
152 {
153 // Default color for the text
154 setFormat(0, text.length(), colorForRole(ColorScheme::Number));
155
156 if (!Settings::instance()->syntaxHighlighting)
157 return;
158
159 if (text.startsWith(QLatin1String("="))) {
160 setFormat(0, 1, colorForRole(ColorScheme::Operator));
161 setFormat(1, text.length(), colorForRole(ColorScheme::Result));
162 if (Settings::instance()->digitGrouping > 0)
163 groupDigits(text, 1, text.length() - 1);
164 return;
165 }
166
167 int questionMarkIndex = text.indexOf('?');
168 if (questionMarkIndex != -1)
169 setFormat(questionMarkIndex, text.length(), colorForRole(ColorScheme::Comment));
170
171 Tokens tokens = Evaluator::instance()->scan(text);
172
173 for (int i = 0; i < tokens.count(); ++i) {
174 const Token& token = tokens.at(i);
175 const QString tokenText = token.text().toLower();
176 QStringList functionNames = FunctionRepo::instance()->getIdentifiers();
177 QColor color;
178
179 switch (token.type()) {
180 case Token::stxNumber:
181 case Token::stxUnknown:
182 color = colorForRole(ColorScheme::Number);
183 // TODO: color thousand separators differently? It might help troubleshooting issues
184 break;
185
186 case Token::stxOperator:
187 color = colorForRole(ColorScheme::Operator);
188 break;
189
190 case Token::stxSep:
191 color = colorForRole(ColorScheme::Separator);
192 break;
193
194 case Token::stxOpenPar:
195 case Token::stxClosePar:
196 color = colorForRole(ColorScheme::Parens);
197 break;
198
199 case Token::stxIdentifier:
200 color = colorForRole(ColorScheme::Variable);
201 if (Evaluator::instance()->hasUserFunction(token.text())
202 || functionNames.contains(tokenText, Qt::CaseInsensitive))
203 color = colorForRole(ColorScheme::Function);
204 break;
205
206 default:
207 break;
208 };
209
210 setFormat(token.pos(), token.size(), color);
211 if (token.type() == Token::stxNumber && Settings::instance()->digitGrouping > 0)
212 groupDigits(text, token.pos(), token.size());
213 }
214 }
215
update()216 void SyntaxHighlighter::update()
217 {
218 QString name = Settings::instance()->colorScheme;
219 setColorScheme(ColorScheme::loadByName(name));
220
221 QColor backgroundColor = colorForRole(ColorScheme::Background);
222 QWidget* parentWidget = static_cast<QWidget*>(parent());
223 QPalette pal = parentWidget->palette();
224 pal.setColor(QPalette::Active, QPalette::Base, backgroundColor);
225 pal.setColor(QPalette::Inactive, QPalette::Base, backgroundColor);
226 parentWidget->setPalette(pal);
227
228 rehighlight();
229 }
230
formatDigitsGroup(const QString & text,int start,int end,bool invert,int size)231 void SyntaxHighlighter::formatDigitsGroup(const QString& text, int start, int end, bool invert, int size)
232 {
233 Q_ASSERT(start <= end);
234 Q_ASSERT(size > 0);
235
236 qreal spacing = 100; // Size of the space between groups (100 means no space).
237 spacing += 40 * Settings::instance()->digitGrouping;
238 int inc = !invert ? -1 : 1;
239 if(!invert)
240 {
241 int tmp = start;
242 start = end - 1;
243 end = tmp - 1;
244
245 // Skip the first digit so that we add the spacing to the first digit of the next group.
246 while (start != end && Evaluator::isSeparatorChar(text[start].unicode()))
247 --start;
248 if (start == end)
249 return; // Bug ?
250 --start;
251 }
252
253 for (int count = 0 ; start != end ; start += inc)
254 {
255 // When there are separators in the number, we must not count them as part of the group.
256 if (!Evaluator::isSeparatorChar(text[start].unicode()))
257 {
258 ++count;
259 if (count == size)
260 {
261 // Only change the letter spacing from the format and keep the other properties.
262 QTextCharFormat fmt = format(start);
263 fmt.setFontLetterSpacing(spacing);
264 setFormat(start, 1, fmt);
265 count = 0; // Reset
266 // TODO: if the next character is a separator, do not add spacing?
267 }
268 }
269 }
270 }
271
groupDigits(const QString & text,int pos,int length)272 void SyntaxHighlighter::groupDigits(const QString& text, int pos, int length)
273 {
274 // Used to find out which characters belong to which radixes.
275 static int charType[128] = { 0 };
276 static const int BIN_CHAR = (1 << 0);
277 static const int OCT_CHAR = (1 << 1);
278 static const int DEC_CHAR = (1 << 2);
279 static const int HEX_CHAR = (1 << 3);
280
281 if (charType[int('0')] == 0) { // Initialize the table on first call (not thread-safe!).
282 charType[int('0')] = HEX_CHAR | DEC_CHAR | OCT_CHAR | BIN_CHAR;
283 charType[int('1')] = HEX_CHAR | DEC_CHAR | OCT_CHAR | BIN_CHAR;
284 charType[int('2')] = HEX_CHAR | DEC_CHAR | OCT_CHAR;
285 charType[int('3')] = HEX_CHAR | DEC_CHAR | OCT_CHAR;
286 charType[int('4')] = HEX_CHAR | DEC_CHAR | OCT_CHAR;
287 charType[int('5')] = HEX_CHAR | DEC_CHAR | OCT_CHAR;
288 charType[int('6')] = HEX_CHAR | DEC_CHAR | OCT_CHAR;
289 charType[int('7')] = HEX_CHAR | DEC_CHAR | OCT_CHAR;
290 charType[int('8')] = HEX_CHAR | DEC_CHAR;
291 charType[int('9')] = HEX_CHAR | DEC_CHAR;
292 charType[int('a')] = HEX_CHAR;
293 charType[int('b')] = HEX_CHAR;
294 charType[int('c')] = HEX_CHAR;
295 charType[int('d')] = HEX_CHAR;
296 charType[int('e')] = HEX_CHAR;
297 charType[int('f')] = HEX_CHAR;
298 charType[int('A')] = HEX_CHAR;
299 charType[int('B')] = HEX_CHAR;
300 charType[int('C')] = HEX_CHAR;
301 charType[int('D')] = HEX_CHAR;
302 charType[int('E')] = HEX_CHAR;
303 charType[int('F')] = HEX_CHAR;
304 }
305
306 int s = -1; // Index of the first digit (most significant).
307 bool invertGroup = false; // If true, group digits from the most significant digit.
308 int groupSize = 3; // Number of digits to group (depends on the radix).
309 int allowedChars = DEC_CHAR; // Allowed characters for the radix of the current number being parsed.
310
311 int endPos = pos + length;
312 if (endPos > text.length())
313 endPos = text.length();
314 for (int i = pos; i < endPos; ++i) {
315 ushort c = text[i].unicode();
316 bool isDigit = c < 128 && (charType[c] & allowedChars);
317
318 if (s >= 0) {
319 if (!isDigit) {
320 bool endOfNumber = true;
321 // If this is a separator and next character is a digit or a separator,
322 // the next character is part of the same number expression
323 if (Evaluator::isSeparatorChar(c) && i<endPos-1) {
324 ushort nextC = text[i+1].unicode();
325 if ((nextC < 128 && (charType[nextC] & allowedChars))
326 || Evaluator::isSeparatorChar(nextC))
327 endOfNumber = false;
328 }
329
330 if (c == ':' || c == 0xB0 || c == '\'' || c == '"')
331 endOfNumber = true;
332
333 if (endOfNumber) {
334 // End of current number found, start grouping the digits.
335 formatDigitsGroup(text, s, i, invertGroup, groupSize);
336 s = -1; // Reset.
337 }
338 }
339 } else {
340 if (isDigit) // Start of number found.
341 s = i;
342 }
343
344 if (!isDigit) {
345 if (Evaluator::isRadixChar(c)) {
346 // Invert the grouping for the fractional part.
347 invertGroup = true;
348 } else if (!Evaluator::isSeparatorChar(c)){
349 // Look for a radix prefix.
350 invertGroup = false;
351 if (i > 0 && text[i - 1] == '0') {
352 if (c == 'x') {
353 groupSize = 4;
354 allowedChars = HEX_CHAR;
355 } else if (c == 'o') {
356 groupSize = 3;
357 allowedChars = OCT_CHAR;
358 } else if (c == 'b') {
359 groupSize = 4;
360 allowedChars = BIN_CHAR;
361 } else {
362 groupSize = 3;
363 allowedChars = DEC_CHAR;
364 }
365 } else {
366 groupSize = 3;
367 allowedChars = DEC_CHAR;
368 }
369 }
370 }
371 }
372
373 // Group the last digits if the string finishes with the number.
374 if (s >= 0) {
375 formatDigitsGroup(text, s, endPos, invertGroup, groupSize);
376 }
377 }
378
379
380 // Original code snippet from StackOverflow.
381 // http://stackoverflow.com/questions/15280452/how-can-i-get-highlighted-text-from-a-qsyntaxhighlighter-into-an-html-string
asHtml(QString & html)382 void SyntaxHighlighter::asHtml(QString& html)
383 {
384 // Create a new document from all the selected text document.
385 QTextCursor cursor(document());
386 cursor.select(QTextCursor::Document);
387 QTextDocument* tempDocument(new QTextDocument);
388 Q_ASSERT(tempDocument);
389 QTextCursor tempCursor(tempDocument);
390
391 tempCursor.insertFragment(cursor.selection());
392 tempCursor.select(QTextCursor::Document);
393 // Set the default foreground for the inserted characters.
394 QTextCharFormat textfmt = tempCursor.charFormat();
395 textfmt.setForeground(Qt::gray);
396 tempCursor.setCharFormat(textfmt);
397
398 // Apply the additional formats set by the syntax highlighter
399 QTextBlock start = document()->findBlock(cursor.selectionStart());
400 QTextBlock end = document()->findBlock(cursor.selectionEnd());
401 end = end.next();
402 const int selectionStart = cursor.selectionStart();
403 const int endOfDocument = tempDocument->characterCount() - 1;
404 for(QTextBlock current = start; current.isValid() && current != end; current = current.next()) {
405 const QTextLayout* layout(current.layout());
406
407 foreach(const QTextLayout::FormatRange &range, layout->additionalFormats()) {
408 const int start = current.position() + range.start - selectionStart;
409 const int end = start + range.length;
410 if(end <= 0 || start >= endOfDocument)
411 continue;
412 tempCursor.setPosition(qMax(start, 0));
413 tempCursor.setPosition(qMin(end, endOfDocument), QTextCursor::KeepAnchor);
414 tempCursor.setCharFormat(range.format);
415 }
416 }
417
418 // Reset the user states since they are not interesting
419 for(QTextBlock block = tempDocument->begin(); block.isValid(); block = block.next())
420 block.setUserState(-1);
421
422 // Make sure the text appears pre-formatted, and set the background we want.
423 tempCursor.select(QTextCursor::Document);
424 QTextBlockFormat blockFormat = tempCursor.blockFormat();
425 blockFormat.setNonBreakableLines(true);
426 blockFormat.setBackground(colorForRole(ColorScheme::Background));
427 tempCursor.setBlockFormat(blockFormat);
428
429 // Finally retreive the syntax higlighted and formatted html.
430 html = tempCursor.selection().toHtml("UTF-8");
431 delete tempDocument;
432
433 // Inject CSS, so to avoid a white margin
434 html.replace("<head>", QString("<head> <style> body {background-color: %1;}</style>")
435 .arg(colorForRole(ColorScheme::Background).name()));
436 }
437