Warning, file /education/cantor/src/lib/defaulthighlighter.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* 0002 SPDX-License-Identifier: GPL-2.0-or-later 0003 SPDX-FileCopyrightText: 2009 Alexander Rieder <alexanderrieder@gmail.com> 0004 SPDX-FileCopyrightText: 2006 David Saxton <david@bluehaze.org> 0005 */ 0006 0007 #include "defaulthighlighter.h" 0008 #include "session.h" 0009 0010 #include <QApplication> 0011 #include <QTextDocument> 0012 #include <QTextCursor> 0013 #include <QGraphicsTextItem> 0014 #include <KColorScheme> 0015 #include <QStack> 0016 0017 using namespace Cantor; 0018 0019 struct HighlightingRule 0020 { 0021 QRegularExpression regExp; 0022 QTextCharFormat format; 0023 }; 0024 0025 bool operator==(const HighlightingRule& rule1, const HighlightingRule& rule2) 0026 { 0027 return rule1.regExp == rule2.regExp; 0028 } 0029 0030 struct PairOpener { 0031 PairOpener() : position(-1), type(-1) { } 0032 PairOpener(int p, int t) : position(p), type(t) { } 0033 0034 int position; 0035 int type; 0036 }; 0037 0038 class Cantor::DefaultHighlighterPrivate 0039 { 0040 public: 0041 QTextCursor cursor; 0042 0043 //Character formats to use for the highlighting 0044 QTextCharFormat functionFormat; 0045 QTextCharFormat variableFormat; 0046 QTextCharFormat objectFormat; 0047 QTextCharFormat keywordFormat; 0048 QTextCharFormat numberFormat; 0049 QTextCharFormat operatorFormat; 0050 QTextCharFormat errorFormat; 0051 QTextCharFormat commentFormat; 0052 QTextCharFormat stringFormat; 0053 QTextCharFormat matchingPairFormat; 0054 QTextCharFormat mismatchingPairFormat; 0055 0056 int lastBlockNumber = -1; 0057 int lastPosition = -1; 0058 bool suppressRuleChangedSignal = false; 0059 0060 // each two consecutive items build a pair 0061 QList<QChar> pairs; 0062 0063 QList<HighlightingRule> regExpRules; 0064 QHash<QString, QTextCharFormat> wordRules; 0065 }; 0066 0067 DefaultHighlighter::DefaultHighlighter(QObject* parent) : QSyntaxHighlighter(parent), 0068 d(new DefaultHighlighterPrivate) 0069 { 0070 addPair(QLatin1Char('('), QLatin1Char(')')); 0071 addPair(QLatin1Char('['), QLatin1Char(']')); 0072 addPair(QLatin1Char('{'), QLatin1Char('}')); 0073 0074 updateFormats(); 0075 connect(qApp, &QGuiApplication::paletteChanged, 0076 this, &DefaultHighlighter::updateFormats); 0077 } 0078 0079 DefaultHighlighter::DefaultHighlighter(QObject* parent, Session* session) 0080 :DefaultHighlighter(parent) 0081 { 0082 if (session) 0083 { 0084 auto* model = session->variableModel(); 0085 if (model) 0086 { 0087 connect(model, &DefaultVariableModel::variablesAdded, this, &DefaultHighlighter::addVariables); 0088 connect(model, &DefaultVariableModel::variablesRemoved, this, &DefaultHighlighter::removeRules); 0089 connect(model, &DefaultVariableModel::functionsAdded, this, &DefaultHighlighter::addFunctions); 0090 connect(model, &DefaultVariableModel::functionsRemoved, this, &DefaultHighlighter::removeRules); 0091 0092 addVariables(model->variableNames()); 0093 addFunctions(model->functions()); 0094 } 0095 } 0096 } 0097 0098 DefaultHighlighter::~DefaultHighlighter() 0099 { 0100 delete d; 0101 } 0102 0103 void DefaultHighlighter::setTextItem(QGraphicsTextItem* item) 0104 { 0105 d->cursor = item->textCursor(); 0106 setDocument(item->document()); 0107 0108 // make sure every item is connected only once 0109 item->disconnect(this, SLOT(positionChanged(QTextCursor))); 0110 0111 // QGraphicsTextItem has no signal cursorPositionChanged, but item really 0112 // is a WorksheetTextItem 0113 connect(item, SIGNAL(cursorPositionChanged(QTextCursor)), 0114 this, SLOT(positionChanged(QTextCursor))); 0115 0116 d->lastBlockNumber = -1; 0117 d->lastPosition = -1; 0118 } 0119 0120 bool DefaultHighlighter::skipHighlighting(const QString& text) 0121 { 0122 return text.isEmpty(); 0123 } 0124 0125 void DefaultHighlighter::highlightBlock(const QString& text) 0126 { 0127 d->lastBlockNumber = d->cursor.blockNumber(); 0128 if (skipHighlighting(text)) 0129 return; 0130 0131 highlightPairs(text); 0132 highlightWords(text); 0133 highlightRegExps(text); 0134 } 0135 0136 void DefaultHighlighter::addPair(QChar openSymbol, QChar closeSymbol) 0137 { 0138 Q_ASSERT(!d->pairs.contains(openSymbol)); 0139 Q_ASSERT(!d->pairs.contains(closeSymbol)); 0140 d->pairs << openSymbol << closeSymbol; 0141 } 0142 void DefaultHighlighter::highlightPairs(const QString& text) 0143 { 0144 const auto& cursor = d->cursor; 0145 int cursorPos = -1; 0146 if (cursor.blockNumber() == currentBlock().blockNumber() ) { 0147 cursorPos = cursor.position() - currentBlock().position(); 0148 // when text changes, this will be called before the positionChanged signal 0149 // gets emitted. Hence update the position so we don't highlight twice 0150 d->lastPosition = cursor.position(); 0151 } 0152 0153 QStack<PairOpener> opened; 0154 0155 for (int i = 0; i < text.size(); ++i) { 0156 int idx = d->pairs.indexOf(text[i]); 0157 if (idx == -1) 0158 continue; 0159 if (idx % 2 == 0) { //opener of a pair 0160 opened.push(PairOpener(i, idx)); 0161 } else if (opened.isEmpty()) { //closer with no previous opener 0162 setFormat(i, 1, errorFormat()); 0163 } else if (opened.top().type == idx - 1) { //closer with matched opener 0164 int openPos = opened.pop().position; 0165 if (cursorPos != -1 && 0166 (openPos == cursorPos || openPos == cursorPos - 1 || 0167 i == cursorPos || i == cursorPos - 1)) { 0168 setFormat(openPos, 1, matchingPairFormat()); 0169 setFormat(i, 1, matchingPairFormat()); 0170 } 0171 } else { //closer with mismatching opener 0172 int openPos = opened.pop().position; 0173 setFormat(openPos, 1, mismatchingPairFormat()); 0174 setFormat(i, 1, mismatchingPairFormat()); 0175 } 0176 } 0177 0178 // handled unterminated pairs 0179 while (!opened.isEmpty()) { 0180 int position = opened.pop().position; 0181 setFormat(position, 1, errorFormat()); 0182 } 0183 } 0184 0185 QStringList Cantor::DefaultHighlighter::parseBlockTextToWords(const QString& text) 0186 { 0187 return text.split(QRegularExpression(QStringLiteral("\\b")), QString::SkipEmptyParts); 0188 } 0189 0190 void DefaultHighlighter::highlightWords(const QString& text) 0191 { 0192 //qDebug() << "DefaultHighlighter::highlightWords"; 0193 0194 const QStringList& words = parseBlockTextToWords(text); 0195 int count; 0196 int pos = 0; 0197 0198 const int n = words.size(); 0199 for (int i = 0; i < n; ++i) 0200 { 0201 count = words[i].size(); 0202 QString word = words[i]; 0203 0204 //kind of a HACK: 0205 //look at previous words, if they end with allowed characters, 0206 //prepend them to the current word. This allows for example 0207 //to highlight words that start with a "Non-word"-character 0208 //e.g. %pi in the scilab backend. 0209 //qDebug() << "nonSeparatingCharacters().isNull(): " << nonSeparatingCharacters().isNull(); 0210 if(!nonSeparatingCharacters().isNull()) 0211 { 0212 for(int j = i - 1; j >= 0; j--) 0213 { 0214 //qDebug() << "j: " << j << "w: " << words[j]; 0215 const QString& w = words[j]; 0216 const QString exp = QStringLiteral("(%1)*$").arg(nonSeparatingCharacters()); 0217 //qDebug() << "exp: " << exp; 0218 int idx = w.indexOf(QRegularExpression(exp)); 0219 const QString& s = w.mid(idx); 0220 //qDebug() << "s: " << s; 0221 0222 if(s.size() > 0) 0223 { 0224 pos -= s.size(); 0225 count += s.size(); 0226 word = s + word; 0227 } else{ 0228 break; 0229 } 0230 } 0231 } 0232 0233 word = word.trimmed(); 0234 0235 //qDebug() << "highlighting: " << word; 0236 0237 if (d->wordRules.contains(word)) 0238 { 0239 setFormat(pos, count, d->wordRules[word]); 0240 } 0241 0242 pos += count; 0243 } 0244 } 0245 0246 void DefaultHighlighter::highlightRegExps(const QString& text) 0247 { 0248 for (const auto& rule : d->regExpRules) 0249 { 0250 auto iter = rule.regExp.globalMatch(text); 0251 while (iter.hasNext()) { 0252 auto match = iter.next(); 0253 setFormat(match.capturedStart(0), match.capturedLength(0), rule.format); 0254 } 0255 } 0256 } 0257 0258 QTextCharFormat DefaultHighlighter::functionFormat() const 0259 { 0260 return d->functionFormat; 0261 } 0262 0263 QTextCharFormat DefaultHighlighter::variableFormat() const 0264 { 0265 return d->variableFormat; 0266 } 0267 0268 QTextCharFormat DefaultHighlighter::objectFormat() const 0269 { 0270 return d->objectFormat; 0271 } 0272 0273 QTextCharFormat DefaultHighlighter::keywordFormat() const 0274 { 0275 return d->keywordFormat; 0276 } 0277 0278 QTextCharFormat DefaultHighlighter::numberFormat() const 0279 { 0280 return d->numberFormat; 0281 } 0282 0283 QTextCharFormat DefaultHighlighter::operatorFormat() const 0284 { 0285 return d->operatorFormat; 0286 } 0287 0288 QTextCharFormat DefaultHighlighter::errorFormat() const 0289 { 0290 return d->errorFormat; 0291 } 0292 0293 QTextCharFormat DefaultHighlighter::commentFormat() const 0294 { 0295 return d->commentFormat; 0296 } 0297 0298 QTextCharFormat DefaultHighlighter::stringFormat() const 0299 { 0300 return d->stringFormat; 0301 } 0302 0303 QTextCharFormat DefaultHighlighter::matchingPairFormat() const 0304 { 0305 return d->matchingPairFormat; 0306 } 0307 0308 QTextCharFormat DefaultHighlighter::mismatchingPairFormat() const 0309 { 0310 return d->mismatchingPairFormat; 0311 } 0312 0313 void DefaultHighlighter::updateFormats() 0314 { 0315 //initialize char-formats 0316 KColorScheme scheme(QPalette::Active); 0317 0318 d->functionFormat.setForeground(scheme.foreground(KColorScheme::LinkText)); 0319 d->functionFormat.setFontWeight(QFont::DemiBold); 0320 0321 d->variableFormat.setForeground(scheme.foreground(KColorScheme::ActiveText)); 0322 0323 d->objectFormat.setForeground(scheme.foreground(KColorScheme::NormalText)); 0324 d->objectFormat.setFontWeight(QFont::Bold); 0325 0326 d->keywordFormat.setForeground(scheme.foreground(KColorScheme::NeutralText)); 0327 d->keywordFormat.setFontWeight(QFont::Bold); 0328 0329 d->numberFormat.setForeground(scheme.foreground(KColorScheme::NeutralText)); 0330 0331 d->operatorFormat.setForeground(scheme.foreground(KColorScheme::NormalText)); 0332 d->operatorFormat.setFontWeight(QFont::Bold); 0333 0334 d->errorFormat.setForeground(scheme.foreground(KColorScheme::NormalText)); 0335 d->errorFormat.setUnderlineColor(scheme.foreground(KColorScheme::NegativeText).color()); 0336 d->errorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); 0337 0338 d->commentFormat.setForeground(scheme.foreground(KColorScheme::InactiveText)); 0339 0340 d->stringFormat.setForeground(scheme.foreground(KColorScheme::PositiveText)); 0341 0342 d->matchingPairFormat.setForeground(scheme.foreground(KColorScheme::NeutralText)); 0343 d->matchingPairFormat.setBackground(scheme.background(KColorScheme::NeutralBackground)); 0344 0345 d->mismatchingPairFormat.setForeground(scheme.foreground(KColorScheme::NegativeText)); 0346 d->mismatchingPairFormat.setBackground(scheme.background(KColorScheme::NegativeBackground)); 0347 } 0348 0349 0350 void DefaultHighlighter::positionChanged(const QTextCursor& cursor) 0351 { 0352 if (!cursor.isNull() && cursor.document() != document()) 0353 // A new item notified us, but we did not yet change our document. 0354 // We are waiting for that to happen. 0355 return; 0356 0357 d->cursor = cursor; 0358 if ( (cursor.isNull() || cursor.blockNumber() != d->lastBlockNumber) && 0359 d->lastBlockNumber >= 0 ) { 0360 // remove highlight from last focused block 0361 rehighlightBlock(document()->findBlockByNumber(d->lastBlockNumber)); 0362 } 0363 0364 if (cursor.isNull()) { 0365 d->lastBlockNumber = -1; 0366 d->lastPosition = -1; 0367 return; 0368 } 0369 0370 d->lastBlockNumber = cursor.blockNumber(); 0371 0372 if ( d->lastPosition == cursor.position() ) { 0373 return; 0374 } 0375 0376 rehighlightBlock(cursor.block()); 0377 d->lastPosition = cursor.position(); 0378 } 0379 0380 void DefaultHighlighter::addRule(const QString& word, const QTextCharFormat& format) 0381 { 0382 d->wordRules[word] = format; 0383 if (!d->suppressRuleChangedSignal) 0384 emit rulesChanged(); 0385 } 0386 0387 void DefaultHighlighter::addRule(const QRegularExpression& regexp, const QTextCharFormat& format) 0388 { 0389 HighlightingRule rule = { regexp, format }; 0390 d->regExpRules.removeAll(rule); 0391 d->regExpRules.append(rule); 0392 if (!d->suppressRuleChangedSignal) 0393 emit rulesChanged(); 0394 } 0395 0396 void DefaultHighlighter::removeRule(const QString& word) 0397 { 0398 d->wordRules.remove(word); 0399 if (!d->suppressRuleChangedSignal) 0400 emit rulesChanged(); 0401 } 0402 0403 void DefaultHighlighter::removeRule(const QRegularExpression& regexp) 0404 { 0405 HighlightingRule rule = { regexp, QTextCharFormat() }; 0406 d->regExpRules.removeAll(rule); 0407 if (!d->suppressRuleChangedSignal) 0408 emit rulesChanged(); 0409 } 0410 0411 void DefaultHighlighter::addRules(const QStringList& conditions, const QTextCharFormat& format) 0412 { 0413 auto i = conditions.constBegin(); 0414 d->suppressRuleChangedSignal = true; 0415 for (;i != conditions.constEnd(); ++i) 0416 { 0417 addRule(*i, format); 0418 } 0419 d->suppressRuleChangedSignal = false; 0420 emit rulesChanged(); 0421 } 0422 0423 void DefaultHighlighter::addFunctions(const QStringList& functions) 0424 { 0425 addRules(functions, functionFormat()); 0426 } 0427 0428 void DefaultHighlighter::addKeywords(const QStringList& keywords) 0429 { 0430 addRules(keywords, keywordFormat()); 0431 } 0432 0433 void DefaultHighlighter::addVariables(const QStringList& variables) 0434 { 0435 addRules(variables, variableFormat()); 0436 } 0437 0438 void DefaultHighlighter::removeRules(const QStringList& conditions) 0439 { 0440 auto i = conditions.constBegin(); 0441 d->suppressRuleChangedSignal = true; 0442 for (;i != conditions.constEnd(); ++i) 0443 { 0444 removeRule(*i); 0445 } 0446 d->suppressRuleChangedSignal = false; 0447 emit rulesChanged(); 0448 } 0449 0450 QString DefaultHighlighter::nonSeparatingCharacters() const 0451 { 0452 return QString(); 0453 }