File indexing completed on 2023-05-30 10:40:15

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 }