File indexing completed on 2024-05-05 16:22:13

0001 /*
0002  * highlighter.cpp
0003  *
0004  * SPDX-FileCopyrightText: 2004 Zack Rusin <zack@kde.org>
0005  * SPDX-FileCopyrightText: 2006 Laurent Montel <montel@kde.org>
0006  * SPDX-FileCopyrightText: 2013 Martin Sandsmark <martin.sandsmark@org>
0007  *
0008  * SPDX-License-Identifier: LGPL-2.1-or-later
0009  */
0010 
0011 #include "highlighter.h"
0012 
0013 #include "languagefilter_p.h"
0014 #include "loader_p.h"
0015 #include "settingsimpl_p.h"
0016 #include "speller.h"
0017 #include "tokenizer_p.h"
0018 
0019 #include "ui_debug.h"
0020 
0021 #include <QColor>
0022 #include <QEvent>
0023 #include <QHash>
0024 #include <QKeyEvent>
0025 #include <QMetaMethod>
0026 #include <QPlainTextEdit>
0027 #include <QTextCharFormat>
0028 #include <QTextCursor>
0029 #include <QTextEdit>
0030 #include <QTimer>
0031 
0032 namespace Sonnet
0033 {
0034 // Cache of previously-determined languages (when using AutoDetectLanguage)
0035 // There is one such cache per block (paragraph)
0036 class LanguageCache : public QTextBlockUserData
0037 {
0038 public:
0039     // Key: QPair<start, length>
0040     // Value: language name
0041     QMap<QPair<int, int>, QString> languages;
0042 
0043     // Remove all cached language information after @p pos
0044     void invalidate(int pos)
0045     {
0046         QMutableMapIterator<QPair<int, int>, QString> it(languages);
0047         it.toBack();
0048         while (it.hasPrevious()) {
0049             it.previous();
0050             if (it.key().first + it.key().second >= pos) {
0051                 it.remove();
0052             } else {
0053                 break;
0054             }
0055         }
0056     }
0057 
0058     QString languageAtPos(int pos) const
0059     {
0060         // The data structure isn't really great for such lookups...
0061         QMapIterator<QPair<int, int>, QString> it(languages);
0062         while (it.hasNext()) {
0063             it.next();
0064             if (it.key().first <= pos && it.key().first + it.key().second >= pos) {
0065                 return it.value();
0066             }
0067         }
0068         return QString();
0069     }
0070 };
0071 
0072 class HighlighterPrivate
0073 {
0074 public:
0075     HighlighterPrivate(Highlighter *qq, const QColor &col)
0076         : textEdit(nullptr)
0077         , plainTextEdit(nullptr)
0078         , spellColor(col)
0079         , q(qq)
0080     {
0081         tokenizer = new WordTokenizer();
0082         active = true;
0083         automatic = false;
0084         autoDetectLanguageDisabled = false;
0085         connected = false;
0086         wordCount = 0;
0087         errorCount = 0;
0088         intraWordEditing = false;
0089         completeRehighlightRequired = false;
0090         spellColor = spellColor.isValid() ? spellColor : Qt::red;
0091         languageFilter = new LanguageFilter(new SentenceTokenizer());
0092 
0093         loader = Loader::openLoader();
0094         loader->settings()->restore();
0095 
0096         spellchecker = new Sonnet::Speller();
0097         spellCheckerFound = spellchecker->isValid();
0098         rehighlightRequest = new QTimer(q);
0099         q->connect(rehighlightRequest, &QTimer::timeout, q, &Highlighter::slotRehighlight);
0100 
0101         if (!spellCheckerFound) {
0102             return;
0103         }
0104 
0105         disablePercentage = loader->settings()->disablePercentageWordError();
0106         disableWordCount = loader->settings()->disableWordErrorCount();
0107 
0108         completeRehighlightRequired = true;
0109         rehighlightRequest->setInterval(0);
0110         rehighlightRequest->setSingleShot(true);
0111         rehighlightRequest->start();
0112     }
0113 
0114     ~HighlighterPrivate();
0115     WordTokenizer *tokenizer = nullptr;
0116     LanguageFilter *languageFilter = nullptr;
0117     Loader *loader = nullptr;
0118     Speller *spellchecker = nullptr;
0119     QTextEdit *textEdit = nullptr;
0120     QPlainTextEdit *plainTextEdit = nullptr;
0121     bool active;
0122     bool automatic;
0123     bool autoDetectLanguageDisabled;
0124     bool completeRehighlightRequired;
0125     bool intraWordEditing;
0126     bool spellCheckerFound; // cached d->dict->isValid() value
0127     bool connected;
0128     int disablePercentage = 0;
0129     int disableWordCount = 0;
0130     int wordCount, errorCount;
0131     QTimer *rehighlightRequest = nullptr;
0132     QColor spellColor;
0133     Highlighter *const q;
0134 };
0135 
0136 HighlighterPrivate::~HighlighterPrivate()
0137 {
0138     delete spellchecker;
0139     delete languageFilter;
0140     delete tokenizer;
0141 }
0142 
0143 Highlighter::Highlighter(QTextEdit *edit, const QColor &_col)
0144     : QSyntaxHighlighter(edit)
0145     , d(new HighlighterPrivate(this, _col))
0146 {
0147     d->textEdit = edit;
0148     d->textEdit->installEventFilter(this);
0149     d->textEdit->viewport()->installEventFilter(this);
0150 }
0151 
0152 Highlighter::Highlighter(QPlainTextEdit *edit, const QColor &col)
0153     : QSyntaxHighlighter(edit)
0154     , d(new HighlighterPrivate(this, col))
0155 {
0156     d->plainTextEdit = edit;
0157     setDocument(d->plainTextEdit->document());
0158     d->plainTextEdit->installEventFilter(this);
0159     d->plainTextEdit->viewport()->installEventFilter(this);
0160 }
0161 
0162 Highlighter::~Highlighter()
0163 {
0164     delete d;
0165 }
0166 
0167 bool Highlighter::spellCheckerFound() const
0168 {
0169     return d->spellCheckerFound;
0170 }
0171 
0172 void Highlighter::slotRehighlight()
0173 {
0174     if (d->completeRehighlightRequired) {
0175         d->wordCount = 0;
0176         d->errorCount = 0;
0177         rehighlight();
0178     } else {
0179         // rehighlight the current para only (undo/redo safe)
0180         QTextCursor cursor;
0181         if (d->textEdit) {
0182             cursor = d->textEdit->textCursor();
0183         } else {
0184             cursor = d->plainTextEdit->textCursor();
0185         }
0186         if (cursor.hasSelection()) {
0187             cursor.clearSelection();
0188         }
0189         cursor.insertText(QString());
0190     }
0191     // if (d->checksDone == d->checksRequested)
0192     // d->completeRehighlightRequired = false;
0193     QTimer::singleShot(0, this, SLOT(slotAutoDetection()));
0194 }
0195 
0196 bool Highlighter::automatic() const
0197 {
0198     return d->automatic;
0199 }
0200 
0201 bool Highlighter::autoDetectLanguageDisabled() const
0202 {
0203     return d->autoDetectLanguageDisabled;
0204 }
0205 
0206 bool Highlighter::intraWordEditing() const
0207 {
0208     return d->intraWordEditing;
0209 }
0210 
0211 void Highlighter::setIntraWordEditing(bool editing)
0212 {
0213     d->intraWordEditing = editing;
0214 }
0215 
0216 void Highlighter::setAutomatic(bool automatic)
0217 {
0218     if (automatic == d->automatic) {
0219         return;
0220     }
0221 
0222     d->automatic = automatic;
0223     if (d->automatic) {
0224         slotAutoDetection();
0225     }
0226 }
0227 
0228 void Highlighter::setAutoDetectLanguageDisabled(bool autoDetectDisabled)
0229 {
0230     d->autoDetectLanguageDisabled = autoDetectDisabled;
0231 }
0232 
0233 void Highlighter::slotAutoDetection()
0234 {
0235     bool savedActive = d->active;
0236 
0237     // don't disable just because 1 of 4 is misspelled.
0238     if (d->automatic && d->wordCount >= 10) {
0239         // tme = Too many errors
0240         /* clang-format off */
0241         bool tme = (d->errorCount >= d->disableWordCount)
0242                    && (d->errorCount * 100 >= d->disablePercentage * d->wordCount);
0243         /* clang-format on */
0244 
0245         if (d->active && tme) {
0246             d->active = false;
0247         } else if (!d->active && !tme) {
0248             d->active = true;
0249         }
0250     }
0251 
0252     if (d->active != savedActive) {
0253         if (d->active) {
0254             Q_EMIT activeChanged(tr("As-you-type spell checking enabled."));
0255         } else {
0256             qCDebug(SONNET_LOG_UI) << "Sonnet: Disabling spell checking, too many errors";
0257             Q_EMIT activeChanged(
0258                 tr("Too many misspelled words. "
0259                    "As-you-type spell checking disabled."));
0260         }
0261 
0262         d->completeRehighlightRequired = true;
0263         d->rehighlightRequest->setInterval(100);
0264         d->rehighlightRequest->setSingleShot(true);
0265     }
0266 }
0267 
0268 void Highlighter::setActive(bool active)
0269 {
0270     if (active == d->active) {
0271         return;
0272     }
0273     d->active = active;
0274     rehighlight();
0275 
0276     if (d->active) {
0277         Q_EMIT activeChanged(tr("As-you-type spell checking enabled."));
0278     } else {
0279         Q_EMIT activeChanged(tr("As-you-type spell checking disabled."));
0280     }
0281 }
0282 
0283 bool Highlighter::isActive() const
0284 {
0285     return d->active;
0286 }
0287 
0288 void Highlighter::contentsChange(int pos, int add, int rem)
0289 {
0290     // Invalidate the cache where the text has changed
0291     const QTextBlock &lastBlock = document()->findBlock(pos + add - rem);
0292     QTextBlock block = document()->findBlock(pos);
0293     do {
0294         LanguageCache *cache = dynamic_cast<LanguageCache *>(block.userData());
0295         if (cache) {
0296             cache->invalidate(pos - block.position());
0297         }
0298         block = block.next();
0299     } while (block.isValid() && block < lastBlock);
0300 }
0301 
0302 static bool hasNotEmptyText(const QString &text)
0303 {
0304     for (int i = 0; i < text.length(); ++i) {
0305         if (!text.at(i).isSpace()) {
0306             return true;
0307         }
0308     }
0309     return false;
0310 }
0311 
0312 void Highlighter::highlightBlock(const QString &text)
0313 {
0314     if (!hasNotEmptyText(text) || !d->active || !d->spellCheckerFound) {
0315         return;
0316     }
0317 
0318     if (!d->connected) {
0319         connect(document(), &QTextDocument::contentsChange, this, &Highlighter::contentsChange);
0320         d->connected = true;
0321     }
0322     QTextCursor cursor;
0323     if (d->textEdit) {
0324         cursor = d->textEdit->textCursor();
0325     } else {
0326         cursor = d->plainTextEdit->textCursor();
0327     }
0328     int index = cursor.position();
0329 
0330     const int lengthPosition = text.length() - 1;
0331 
0332     if (index != lengthPosition //
0333         || (lengthPosition > 0 && !text[lengthPosition - 1].isLetter())) {
0334         d->languageFilter->setBuffer(text);
0335 
0336         LanguageCache *cache = dynamic_cast<LanguageCache *>(currentBlockUserData());
0337         if (!cache) {
0338             cache = new LanguageCache;
0339             setCurrentBlockUserData(cache);
0340         }
0341 
0342         const bool autodetectLanguage = d->spellchecker->testAttribute(Speller::AutoDetectLanguage);
0343         while (d->languageFilter->hasNext()) {
0344             Token sentence = d->languageFilter->next();
0345             if (autodetectLanguage && !d->autoDetectLanguageDisabled) {
0346                 QString lang;
0347                 QPair<int, int> spos = QPair<int, int>(sentence.position(), sentence.length());
0348                 // try cache first
0349                 if (cache->languages.contains(spos)) {
0350                     lang = cache->languages.value(spos);
0351                 } else {
0352                     lang = d->languageFilter->language();
0353                     if (!d->languageFilter->isSpellcheckable()) {
0354                         lang.clear();
0355                     }
0356                     cache->languages[spos] = lang;
0357                 }
0358                 if (lang.isEmpty()) {
0359                     continue;
0360                 }
0361                 d->spellchecker->setLanguage(lang);
0362             }
0363 
0364             d->tokenizer->setBuffer(sentence.toString());
0365             int offset = sentence.position();
0366             while (d->tokenizer->hasNext()) {
0367                 Token word = d->tokenizer->next();
0368                 if (!d->tokenizer->isSpellcheckable()) {
0369                     continue;
0370                 }
0371                 ++d->wordCount;
0372                 if (d->spellchecker->isMisspelled(word.toString())) {
0373                     ++d->errorCount;
0374                     setMisspelled(word.position() + offset, word.length());
0375                 } else {
0376                     unsetMisspelled(word.position() + offset, word.length());
0377                 }
0378             }
0379         }
0380     }
0381     // QTimer::singleShot( 0, this, SLOT(checkWords()) );
0382     setCurrentBlockState(0);
0383 }
0384 
0385 QString Highlighter::currentLanguage() const
0386 {
0387     return d->spellchecker->language();
0388 }
0389 
0390 void Highlighter::setCurrentLanguage(const QString &lang)
0391 {
0392     QString prevLang = d->spellchecker->language();
0393     d->spellchecker->setLanguage(lang);
0394     d->spellCheckerFound = d->spellchecker->isValid();
0395     if (!d->spellCheckerFound) {
0396         qCDebug(SONNET_LOG_UI) << "No dictionary for \"" << lang << "\" staying with the current language.";
0397         d->spellchecker->setLanguage(prevLang);
0398         return;
0399     }
0400     d->wordCount = 0;
0401     d->errorCount = 0;
0402     if (d->automatic || d->active) {
0403         d->rehighlightRequest->start(0);
0404     }
0405 }
0406 
0407 void Highlighter::setMisspelled(int start, int count)
0408 {
0409     QTextCharFormat format;
0410     format.setFontUnderline(true);
0411     format.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
0412     format.setUnderlineColor(d->spellColor);
0413     setFormat(start, count, format);
0414 }
0415 
0416 void Highlighter::unsetMisspelled(int start, int count)
0417 {
0418     setFormat(start, count, QTextCharFormat());
0419 }
0420 
0421 bool Highlighter::eventFilter(QObject *o, QEvent *e)
0422 {
0423     if (!d->spellCheckerFound) {
0424         return false;
0425     }
0426     if ((o == d->textEdit || o == d->plainTextEdit) && (e->type() == QEvent::KeyPress)) {
0427         QKeyEvent *k = static_cast<QKeyEvent *>(e);
0428         // d->autoReady = true;
0429         if (d->rehighlightRequest->isActive()) { // try to stay out of the users way
0430             d->rehighlightRequest->start(500);
0431         }
0432         /* clang-format off */
0433         if (k->key() == Qt::Key_Enter
0434             || k->key() == Qt::Key_Return
0435             || k->key() == Qt::Key_Up
0436             || k->key() == Qt::Key_Down
0437             || k->key() == Qt::Key_Left
0438             || k->key() == Qt::Key_Right
0439             || k->key() == Qt::Key_PageUp
0440             || k->key() == Qt::Key_PageDown
0441             || k->key() == Qt::Key_Home
0442             || k->key() == Qt::Key_End
0443             || (k->modifiers() == Qt::ControlModifier
0444                 && (k->key() == Qt::Key_A
0445                     || k->key() == Qt::Key_B
0446                     || k->key() == Qt::Key_E
0447                     || k->key() == Qt::Key_N
0448                     || k->key() == Qt::Key_P))) { /* clang-format on */
0449             if (intraWordEditing()) {
0450                 setIntraWordEditing(false);
0451                 d->completeRehighlightRequired = true;
0452                 d->rehighlightRequest->setInterval(500);
0453                 d->rehighlightRequest->setSingleShot(true);
0454                 d->rehighlightRequest->start();
0455             }
0456         } else {
0457             setIntraWordEditing(true);
0458         }
0459         if (k->key() == Qt::Key_Space //
0460             || k->key() == Qt::Key_Enter //
0461             || k->key() == Qt::Key_Return) {
0462             QTimer::singleShot(0, this, SLOT(slotAutoDetection()));
0463         }
0464     } else if (((d->textEdit && (o == d->textEdit->viewport())) //
0465                 || (d->plainTextEdit && (o == d->plainTextEdit->viewport()))) //
0466                && (e->type() == QEvent::MouseButtonPress)) {
0467         // d->autoReady = true;
0468         if (intraWordEditing()) {
0469             setIntraWordEditing(false);
0470             d->completeRehighlightRequired = true;
0471             d->rehighlightRequest->setInterval(0);
0472             d->rehighlightRequest->setSingleShot(true);
0473             d->rehighlightRequest->start();
0474         }
0475     }
0476     return false;
0477 }
0478 
0479 void Highlighter::addWordToDictionary(const QString &word)
0480 {
0481     d->spellchecker->addToPersonal(word);
0482 }
0483 
0484 void Highlighter::ignoreWord(const QString &word)
0485 {
0486     d->spellchecker->addToSession(word);
0487 }
0488 
0489 QStringList Highlighter::suggestionsForWord(const QString &word, int max)
0490 {
0491     QStringList suggestions = d->spellchecker->suggest(word);
0492     if (max >= 0 && suggestions.count() > max) {
0493         suggestions = suggestions.mid(0, max);
0494     }
0495     return suggestions;
0496 }
0497 
0498 QStringList Highlighter::suggestionsForWord(const QString &word, const QTextCursor &cursor, int max)
0499 {
0500     LanguageCache *cache = dynamic_cast<LanguageCache *>(cursor.block().userData());
0501     if (cache) {
0502         const QString cachedLanguage = cache->languageAtPos(cursor.positionInBlock());
0503         if (!cachedLanguage.isEmpty()) {
0504             d->spellchecker->setLanguage(cachedLanguage);
0505         }
0506     }
0507     QStringList suggestions = d->spellchecker->suggest(word);
0508     if (max >= 0 && suggestions.count() > max) {
0509         suggestions = suggestions.mid(0, max);
0510     }
0511     return suggestions;
0512 }
0513 
0514 bool Highlighter::isWordMisspelled(const QString &word)
0515 {
0516     return d->spellchecker->isMisspelled(word);
0517 }
0518 
0519 void Highlighter::setMisspelledColor(const QColor &color)
0520 {
0521     d->spellColor = color;
0522 }
0523 
0524 bool Highlighter::checkerEnabledByDefault() const
0525 {
0526     return d->loader->settings()->checkerEnabledByDefault();
0527 }
0528 
0529 void Highlighter::setDocument(QTextDocument *document)
0530 {
0531     d->connected = false;
0532     QSyntaxHighlighter::setDocument(document);
0533 }
0534 }
0535 
0536 #include "moc_highlighter.cpp"