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