File indexing completed on 2024-04-28 15:30:45

0001 /*
0002     SPDX-FileCopyrightText: 2008-2010 Michel Ludwig <michel.ludwig@kdemail.net>
0003     SPDX-FileCopyrightText: 2009 Joseph Wenninger <jowenn@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 #include "ontheflycheck.h"
0008 
0009 #include <QRegularExpression>
0010 #include <QTimer>
0011 
0012 #include "katebuffer.h"
0013 #include "kateconfig.h"
0014 #include "kateglobal.h"
0015 #include "katepartdebug.h"
0016 #include "katerenderer.h"
0017 #include "kateview.h"
0018 #include "spellcheck.h"
0019 #include "spellingmenu.h"
0020 
0021 #define ON_THE_FLY_DEBUG qCDebug(LOG_KTE)
0022 
0023 namespace
0024 {
0025 inline const QPair<KTextEditor::MovingRange *, QString> &invalidSpellCheckQueueItem()
0026 {
0027     static const auto item = QPair<KTextEditor::MovingRange *, QString>(nullptr, QString());
0028     return item;
0029 }
0030 
0031 }
0032 
0033 KateOnTheFlyChecker::KateOnTheFlyChecker(KTextEditor::DocumentPrivate *document)
0034     : QObject(document)
0035     , m_document(document)
0036     , m_backgroundChecker(nullptr)
0037     , m_currentlyCheckedItem(invalidSpellCheckQueueItem())
0038     , m_refreshView(nullptr)
0039 {
0040     ON_THE_FLY_DEBUG << "created";
0041 
0042     m_viewRefreshTimer = new QTimer(this);
0043     m_viewRefreshTimer->setSingleShot(true);
0044     connect(m_viewRefreshTimer, &QTimer::timeout, this, &KateOnTheFlyChecker::viewRefreshTimeout);
0045 
0046     connect(document, &KTextEditor::DocumentPrivate::textInsertedRange, this, &KateOnTheFlyChecker::textInserted);
0047     connect(document, &KTextEditor::DocumentPrivate::textRemoved, this, &KateOnTheFlyChecker::textRemoved);
0048     connect(document, &KTextEditor::DocumentPrivate::viewCreated, this, &KateOnTheFlyChecker::addView);
0049     connect(document, &KTextEditor::DocumentPrivate::highlightingModeChanged, this, &KateOnTheFlyChecker::updateConfig);
0050     connect(&document->buffer(), &KateBuffer::respellCheckBlock, this, &KateOnTheFlyChecker::handleRespellCheckBlock);
0051 
0052     connect(document, &KTextEditor::Document::reloaded, this, [this](KTextEditor::Document *) {
0053         refreshSpellCheck();
0054     });
0055 
0056     // load the settings for the speller
0057     updateConfig();
0058 
0059     const auto views = document->views();
0060     for (KTextEditor::View *view : views) {
0061         addView(document, view);
0062     }
0063     refreshSpellCheck();
0064 }
0065 
0066 KateOnTheFlyChecker::~KateOnTheFlyChecker()
0067 {
0068     freeDocument();
0069 }
0070 
0071 QPair<KTextEditor::Range, QString> KateOnTheFlyChecker::getMisspelledItem(const KTextEditor::Cursor cursor) const
0072 {
0073     for (const MisspelledItem &item : m_misspelledList) {
0074         KTextEditor::MovingRange *movingRange = item.first;
0075         if (movingRange->contains(cursor)) {
0076             return QPair<KTextEditor::Range, QString>(*movingRange, item.second);
0077         }
0078     }
0079     return QPair<KTextEditor::Range, QString>(KTextEditor::Range::invalid(), QString());
0080 }
0081 
0082 QString KateOnTheFlyChecker::dictionaryForMisspelledRange(KTextEditor::Range range) const
0083 {
0084     for (const MisspelledItem &item : m_misspelledList) {
0085         KTextEditor::MovingRange *movingRange = item.first;
0086         if (*movingRange == range) {
0087             return item.second;
0088         }
0089     }
0090     return QString();
0091 }
0092 
0093 void KateOnTheFlyChecker::clearMisspellingForWord(const QString &word)
0094 {
0095     const MisspelledList misspelledList = m_misspelledList; // make a copy
0096     for (const MisspelledItem &item : misspelledList) {
0097         KTextEditor::MovingRange *movingRange = item.first;
0098         if (m_document->text(*movingRange) == word) {
0099             deleteMovingRange(movingRange);
0100         }
0101     }
0102 }
0103 
0104 void KateOnTheFlyChecker::handleRespellCheckBlock(int start, int end)
0105 {
0106     ON_THE_FLY_DEBUG << start << end;
0107     KTextEditor::Range range(start, 0, end, m_document->lineLength(end));
0108     bool listEmpty = m_modificationList.isEmpty();
0109     KTextEditor::MovingRange *movingRange = m_document->newMovingRange(range);
0110     movingRange->setFeedback(this);
0111     // we don't handle this directly as the highlighting information might not be up-to-date yet
0112     m_modificationList.push_back(ModificationItem(TEXT_INSERTED, movingRange));
0113     ON_THE_FLY_DEBUG << "added" << *movingRange;
0114     if (listEmpty) {
0115         QTimer::singleShot(0, this, SLOT(handleModifiedRanges()));
0116     }
0117 }
0118 
0119 void KateOnTheFlyChecker::textInserted(KTextEditor::Document *document, KTextEditor::Range range)
0120 {
0121     Q_ASSERT(document == m_document);
0122     Q_UNUSED(document);
0123     if (!range.isValid()) {
0124         return;
0125     }
0126 
0127     bool listEmptyAtStart = m_modificationList.isEmpty();
0128 
0129     // don't consider a range that is not within the document range
0130     const KTextEditor::Range documentIntersection = m_document->documentRange().intersect(range);
0131     if (!documentIntersection.isValid()) {
0132         return;
0133     }
0134     // for performance reasons we only want to schedule spellchecks for ranges that are visible
0135     const auto views = m_document->views();
0136     for (KTextEditor::View *i : views) {
0137         KTextEditor::ViewPrivate *view = static_cast<KTextEditor::ViewPrivate *>(i);
0138         KTextEditor::Range visibleIntersection = documentIntersection.intersect(view->visibleRange());
0139         if (visibleIntersection.isValid()) { // allow empty intersections
0140             // we don't handle this directly as the highlighting information might not be up-to-date yet
0141             KTextEditor::MovingRange *movingRange = m_document->newMovingRange(visibleIntersection);
0142             movingRange->setFeedback(this);
0143             m_modificationList.push_back(ModificationItem(TEXT_INSERTED, movingRange));
0144             ON_THE_FLY_DEBUG << "added" << *movingRange;
0145         }
0146     }
0147 
0148     if (listEmptyAtStart && !m_modificationList.isEmpty()) {
0149         QTimer::singleShot(0, this, SLOT(handleModifiedRanges()));
0150     }
0151 }
0152 
0153 void KateOnTheFlyChecker::handleInsertedText(KTextEditor::Range range)
0154 {
0155     KTextEditor::Range consideredRange = range;
0156     ON_THE_FLY_DEBUG << m_document << range;
0157 
0158     bool spellCheckInProgress = m_currentlyCheckedItem != invalidSpellCheckQueueItem();
0159 
0160     if (spellCheckInProgress) {
0161         KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first;
0162         if (spellCheckRange->contains(consideredRange)) {
0163             consideredRange = *spellCheckRange;
0164             stopCurrentSpellCheck();
0165             deleteMovingRangeQuickly(spellCheckRange);
0166         } else if (consideredRange.contains(*spellCheckRange)) {
0167             stopCurrentSpellCheck();
0168             deleteMovingRangeQuickly(spellCheckRange);
0169         } else if (consideredRange.overlaps(*spellCheckRange)) {
0170             consideredRange.expandToRange(*spellCheckRange);
0171             stopCurrentSpellCheck();
0172             deleteMovingRangeQuickly(spellCheckRange);
0173         } else {
0174             spellCheckInProgress = false;
0175         }
0176     }
0177     for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) {
0178         KTextEditor::MovingRange *spellCheckRange = (*i).first;
0179         if (spellCheckRange->contains(consideredRange)) {
0180             consideredRange = *spellCheckRange;
0181             ON_THE_FLY_DEBUG << "erasing range " << *i;
0182             i = m_spellCheckQueue.erase(i);
0183             deleteMovingRangeQuickly(spellCheckRange);
0184         } else if (consideredRange.contains(*spellCheckRange)) {
0185             ON_THE_FLY_DEBUG << "erasing range " << *i;
0186             i = m_spellCheckQueue.erase(i);
0187             deleteMovingRangeQuickly(spellCheckRange);
0188         } else if (consideredRange.overlaps(*spellCheckRange)) {
0189             consideredRange.expandToRange(*spellCheckRange);
0190             ON_THE_FLY_DEBUG << "erasing range " << *i;
0191             i = m_spellCheckQueue.erase(i);
0192             deleteMovingRangeQuickly(spellCheckRange);
0193         } else {
0194             ++i;
0195         }
0196     }
0197     KTextEditor::Range spellCheckRange = findWordBoundaries(consideredRange.start(), consideredRange.end());
0198     const bool emptyAtStart = m_spellCheckQueue.isEmpty();
0199 
0200     queueSpellCheckVisibleRange(spellCheckRange);
0201 
0202     if (spellCheckInProgress || (emptyAtStart && !m_spellCheckQueue.isEmpty())) {
0203         QTimer::singleShot(0, this, SLOT(performSpellCheck()));
0204     }
0205 }
0206 
0207 void KateOnTheFlyChecker::textRemoved(KTextEditor::Document *document, KTextEditor::Range range)
0208 {
0209     Q_ASSERT(document == m_document);
0210     Q_UNUSED(document);
0211     if (!range.isValid()) {
0212         return;
0213     }
0214 
0215     bool listEmptyAtStart = m_modificationList.isEmpty();
0216 
0217     // don't consider a range that is behind the end of the document
0218     const KTextEditor::Range documentIntersection = m_document->documentRange().intersect(range);
0219     if (!documentIntersection.isValid()) { // the intersection might however be empty if the last
0220         return; // word has been removed, for example
0221     }
0222 
0223     // for performance reasons we only want to schedule spellchecks for ranges that are visible
0224     const auto views = m_document->views();
0225     for (KTextEditor::View *i : views) {
0226         KTextEditor::ViewPrivate *view = static_cast<KTextEditor::ViewPrivate *>(i);
0227         KTextEditor::Range visibleIntersection = documentIntersection.intersect(view->visibleRange());
0228         if (visibleIntersection.isValid()) { // see above
0229             // we don't handle this directly as the highlighting information might not be up-to-date yet
0230             KTextEditor::MovingRange *movingRange = m_document->newMovingRange(visibleIntersection);
0231             movingRange->setFeedback(this);
0232             m_modificationList.push_back(ModificationItem(TEXT_REMOVED, movingRange));
0233             ON_THE_FLY_DEBUG << "added" << *movingRange << view->visibleRange();
0234         }
0235     }
0236     if (listEmptyAtStart && !m_modificationList.isEmpty()) {
0237         QTimer::singleShot(0, this, SLOT(handleModifiedRanges()));
0238     }
0239 }
0240 
0241 inline bool rangesAdjacent(KTextEditor::Range r1, KTextEditor::Range r2)
0242 {
0243     return (r1.end() == r2.start()) || (r2.end() == r1.start());
0244 }
0245 
0246 void KateOnTheFlyChecker::handleRemovedText(KTextEditor::Range range)
0247 {
0248     ON_THE_FLY_DEBUG << range;
0249 
0250     QList<KTextEditor::Range> rangesToReCheck;
0251     for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) {
0252         KTextEditor::MovingRange *spellCheckRange = (*i).first;
0253         if (rangesAdjacent(*spellCheckRange, range) || spellCheckRange->contains(range)) {
0254             ON_THE_FLY_DEBUG << "erasing range " << *i;
0255             if (!spellCheckRange->isEmpty()) {
0256                 rangesToReCheck.push_back(*spellCheckRange);
0257             }
0258             deleteMovingRangeQuickly(spellCheckRange);
0259             i = m_spellCheckQueue.erase(i);
0260         } else {
0261             ++i;
0262         }
0263     }
0264     bool spellCheckInProgress = m_currentlyCheckedItem != invalidSpellCheckQueueItem();
0265     const bool emptyAtStart = m_spellCheckQueue.isEmpty();
0266     if (spellCheckInProgress) {
0267         KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first;
0268         ON_THE_FLY_DEBUG << *spellCheckRange;
0269         if (m_document->documentRange().contains(*spellCheckRange) && (rangesAdjacent(*spellCheckRange, range) || spellCheckRange->contains(range))
0270             && !spellCheckRange->isEmpty()) {
0271             rangesToReCheck.push_back(*spellCheckRange);
0272             ON_THE_FLY_DEBUG << "added the range " << *spellCheckRange;
0273             stopCurrentSpellCheck();
0274             deleteMovingRangeQuickly(spellCheckRange);
0275         } else if (spellCheckRange->isEmpty()) {
0276             stopCurrentSpellCheck();
0277             deleteMovingRangeQuickly(spellCheckRange);
0278         } else {
0279             spellCheckInProgress = false;
0280         }
0281     }
0282     for (QList<KTextEditor::Range>::iterator i = rangesToReCheck.begin(); i != rangesToReCheck.end(); ++i) {
0283         queueSpellCheckVisibleRange(*i);
0284     }
0285 
0286     KTextEditor::Range spellCheckRange = findWordBoundaries(range.start(), range.start());
0287     KTextEditor::Cursor spellCheckEnd = spellCheckRange.end();
0288 
0289     queueSpellCheckVisibleRange(spellCheckRange);
0290 
0291     if (range.numberOfLines() > 0) {
0292         // FIXME: there is no currently no way of doing this better as we only get notifications for removals of
0293         //       of single lines, i.e. we don't know here how many lines have been removed in total
0294         KTextEditor::Cursor nextLineStart(spellCheckEnd.line() + 1, 0);
0295         const KTextEditor::Cursor documentEnd = m_document->documentEnd();
0296         if (nextLineStart < documentEnd) {
0297             KTextEditor::Range rangeBelow = KTextEditor::Range(nextLineStart, documentEnd);
0298 
0299             const QList<KTextEditor::View *> &viewList = m_document->views();
0300             for (QList<KTextEditor::View *>::const_iterator i = viewList.begin(); i != viewList.end(); ++i) {
0301                 KTextEditor::ViewPrivate *view = static_cast<KTextEditor::ViewPrivate *>(*i);
0302                 const KTextEditor::Range visibleRange = view->visibleRange();
0303                 KTextEditor::Range intersection = visibleRange.intersect(rangeBelow);
0304                 if (intersection.isValid()) {
0305                     queueSpellCheckVisibleRange(view, intersection);
0306                 }
0307             }
0308         }
0309     }
0310 
0311     ON_THE_FLY_DEBUG << "finished";
0312     if (spellCheckInProgress || (emptyAtStart && !m_spellCheckQueue.isEmpty())) {
0313         QTimer::singleShot(0, this, SLOT(performSpellCheck()));
0314     }
0315 }
0316 
0317 void KateOnTheFlyChecker::freeDocument()
0318 {
0319     ON_THE_FLY_DEBUG;
0320 
0321     // empty the spell check queue
0322     for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) {
0323         ON_THE_FLY_DEBUG << "erasing range " << *i;
0324         KTextEditor::MovingRange *movingRange = (*i).first;
0325         deleteMovingRangeQuickly(movingRange);
0326         i = m_spellCheckQueue.erase(i);
0327     }
0328     if (m_currentlyCheckedItem != invalidSpellCheckQueueItem()) {
0329         KTextEditor::MovingRange *movingRange = m_currentlyCheckedItem.first;
0330         deleteMovingRangeQuickly(movingRange);
0331     }
0332     stopCurrentSpellCheck();
0333 
0334     const MisspelledList misspelledList = m_misspelledList; // make a copy!
0335     for (const MisspelledItem &i : misspelledList) {
0336         deleteMovingRange(i.first);
0337     }
0338     m_misspelledList.clear();
0339     clearModificationList();
0340 }
0341 
0342 void KateOnTheFlyChecker::performSpellCheck()
0343 {
0344     if (m_currentlyCheckedItem != invalidSpellCheckQueueItem()) {
0345         ON_THE_FLY_DEBUG << "exited as a check is currently in progress";
0346         return;
0347     }
0348     if (m_spellCheckQueue.isEmpty()) {
0349         ON_THE_FLY_DEBUG << "exited as there is nothing to do";
0350         return;
0351     }
0352     m_currentlyCheckedItem = m_spellCheckQueue.takeFirst();
0353 
0354     KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first;
0355     const QString &language = m_currentlyCheckedItem.second;
0356     ON_THE_FLY_DEBUG << "for the range " << *spellCheckRange;
0357     // clear all the highlights that are currently present in the range that
0358     // is supposed to be checked
0359     const MovingRangeList highlightsList = installedMovingRanges(*spellCheckRange); // make a copy!
0360     deleteMovingRanges(highlightsList);
0361 
0362     m_currentDecToEncOffsetList.clear();
0363     KTextEditor::DocumentPrivate::OffsetList encToDecOffsetList;
0364     QString text = m_document->decodeCharacters(*spellCheckRange, m_currentDecToEncOffsetList, encToDecOffsetList);
0365     ON_THE_FLY_DEBUG << "next spell checking" << text;
0366     if (text.isEmpty()) { // passing an empty string to Sonnet can lead to a bad allocation exception
0367         spellCheckDone(); // (bug 225867)
0368         return;
0369     }
0370     if (m_speller.language() != language) {
0371         m_speller.setLanguage(language);
0372     }
0373     if (!m_backgroundChecker) {
0374         m_backgroundChecker = new Sonnet::BackgroundChecker(m_speller, this);
0375         connect(m_backgroundChecker, &Sonnet::BackgroundChecker::misspelling, this, &KateOnTheFlyChecker::misspelling);
0376         connect(m_backgroundChecker, &Sonnet::BackgroundChecker::done, this, &KateOnTheFlyChecker::spellCheckDone);
0377 
0378         KateSpellCheckManager *m_spellCheckManager = KTextEditor::EditorPrivate::self()->spellCheckManager();
0379         connect(m_spellCheckManager, &KateSpellCheckManager::wordAddedToDictionary, this, &KateOnTheFlyChecker::addToDictionary);
0380         connect(m_spellCheckManager, &KateSpellCheckManager::wordIgnored, this, &KateOnTheFlyChecker::addToSession);
0381     }
0382     m_backgroundChecker->setSpeller(m_speller);
0383     m_backgroundChecker->setText(text); // don't call 'start()' after this!
0384 }
0385 
0386 void KateOnTheFlyChecker::addToDictionary(const QString &word)
0387 {
0388     if (m_backgroundChecker) {
0389         m_backgroundChecker->addWordToPersonal(word);
0390     }
0391 }
0392 
0393 void KateOnTheFlyChecker::addToSession(const QString &word)
0394 {
0395     if (m_backgroundChecker) {
0396         m_backgroundChecker->addWordToSession(word);
0397     }
0398 }
0399 
0400 void KateOnTheFlyChecker::removeRangeFromEverything(KTextEditor::MovingRange *movingRange)
0401 {
0402     Q_ASSERT(m_document == movingRange->document());
0403     ON_THE_FLY_DEBUG << *movingRange << "(" << movingRange << ")";
0404 
0405     if (removeRangeFromModificationList(movingRange)) {
0406         return; // range was part of the modification queue, so we don't have
0407         // to look further for it
0408     }
0409 
0410     if (removeRangeFromSpellCheckQueue(movingRange)) {
0411         return; // range was part of the spell check queue, so it cannot have been
0412         // a misspelled range
0413     }
0414 
0415     for (MisspelledList::iterator i = m_misspelledList.begin(); i != m_misspelledList.end();) {
0416         if ((*i).first == movingRange) {
0417             i = m_misspelledList.erase(i);
0418         } else {
0419             ++i;
0420         }
0421     }
0422 }
0423 
0424 bool KateOnTheFlyChecker::removeRangeFromCurrentSpellCheck(KTextEditor::MovingRange *range)
0425 {
0426     if (m_currentlyCheckedItem != invalidSpellCheckQueueItem() && m_currentlyCheckedItem.first == range) {
0427         stopCurrentSpellCheck();
0428         return true;
0429     }
0430     return false;
0431 }
0432 
0433 void KateOnTheFlyChecker::stopCurrentSpellCheck()
0434 {
0435     m_currentDecToEncOffsetList.clear();
0436     m_currentlyCheckedItem = invalidSpellCheckQueueItem();
0437     if (m_backgroundChecker) {
0438         m_backgroundChecker->stop();
0439     }
0440 }
0441 
0442 bool KateOnTheFlyChecker::removeRangeFromSpellCheckQueue(KTextEditor::MovingRange *range)
0443 {
0444     if (removeRangeFromCurrentSpellCheck(range)) {
0445         if (!m_spellCheckQueue.isEmpty()) {
0446             QTimer::singleShot(0, this, SLOT(performSpellCheck()));
0447         }
0448         return true;
0449     }
0450     bool found = false;
0451     for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) {
0452         if ((*i).first == range) {
0453             i = m_spellCheckQueue.erase(i);
0454             found = true;
0455         } else {
0456             ++i;
0457         }
0458     }
0459     return found;
0460 }
0461 
0462 void KateOnTheFlyChecker::rangeEmpty(KTextEditor::MovingRange *range)
0463 {
0464     ON_THE_FLY_DEBUG << range->start() << range->end() << "(" << range << ")";
0465     deleteMovingRange(range);
0466 }
0467 
0468 void KateOnTheFlyChecker::rangeInvalid(KTextEditor::MovingRange *range)
0469 {
0470     ON_THE_FLY_DEBUG << range->start() << range->end() << "(" << range << ")";
0471     deleteMovingRange(range);
0472 }
0473 
0474 /**
0475  * It is not enough to use 'caret/Entered/ExitedRange' only as the cursor doesn't move when some
0476  * text has been selected.
0477  **/
0478 void KateOnTheFlyChecker::caretEnteredRange(KTextEditor::MovingRange *range, KTextEditor::View *view)
0479 {
0480     KTextEditor::ViewPrivate *kateView = static_cast<KTextEditor::ViewPrivate *>(view);
0481     kateView->spellingMenu()->caretEnteredMisspelledRange(range);
0482 }
0483 
0484 void KateOnTheFlyChecker::caretExitedRange(KTextEditor::MovingRange *range, KTextEditor::View *view)
0485 {
0486     KTextEditor::ViewPrivate *kateView = static_cast<KTextEditor::ViewPrivate *>(view);
0487     kateView->spellingMenu()->caretExitedMisspelledRange(range);
0488 }
0489 
0490 void KateOnTheFlyChecker::deleteMovingRange(KTextEditor::MovingRange *range)
0491 {
0492     ON_THE_FLY_DEBUG << range;
0493     // remove it from all our structures
0494     removeRangeFromEverything(range);
0495     range->setFeedback(nullptr);
0496     const auto views = m_document->views();
0497     for (KTextEditor::View *view : views) {
0498         static_cast<KTextEditor::ViewPrivate *>(view)->spellingMenu()->rangeDeleted(range);
0499     }
0500     delete (range);
0501 }
0502 
0503 void KateOnTheFlyChecker::deleteMovingRanges(const QList<KTextEditor::MovingRange *> &list)
0504 {
0505     for (KTextEditor::MovingRange *r : list) {
0506         deleteMovingRange(r);
0507     }
0508 }
0509 
0510 KTextEditor::Range KateOnTheFlyChecker::findWordBoundaries(const KTextEditor::Cursor begin, const KTextEditor::Cursor end)
0511 {
0512     // FIXME: QTextBoundaryFinder should be ideally used for this, but it is currently
0513     //        still broken in Qt
0514     static const QRegularExpression boundaryRegExp(QStringLiteral("\\b"), QRegularExpression::UseUnicodePropertiesOption);
0515     // handle spell checking of QLatin1String("isn't"), QLatin1String("doesn't"), etc.
0516     static const QRegularExpression boundaryQuoteRegExp(QStringLiteral("\\b\\w+'\\w*$"), QRegularExpression::UseUnicodePropertiesOption);
0517     static const QRegularExpression extendedBoundaryRegExp(QStringLiteral("\\W|$"), QRegularExpression::UseUnicodePropertiesOption);
0518     static const QRegularExpression extendedBoundaryQuoteRegExp(QStringLiteral("^\\w*'\\w+\\b"), QRegularExpression::UseUnicodePropertiesOption); // see above
0519     KTextEditor::DocumentPrivate::OffsetList decToEncOffsetList;
0520     KTextEditor::DocumentPrivate::OffsetList encToDecOffsetList;
0521     const int startLine = begin.line();
0522     const int startColumn = begin.column();
0523     KTextEditor::Cursor boundaryStart;
0524     KTextEditor::Cursor boundaryEnd;
0525     // first we take care of the start position
0526     const KTextEditor::Range startLineRange(startLine, 0, startLine, m_document->lineLength(startLine));
0527     QString decodedLineText = m_document->decodeCharacters(startLineRange, decToEncOffsetList, encToDecOffsetList);
0528     int translatedColumn = m_document->computePositionWrtOffsets(encToDecOffsetList, startColumn);
0529     QString text = decodedLineText.mid(0, translatedColumn);
0530     boundaryStart.setLine(startLine);
0531     int match = text.lastIndexOf(boundaryQuoteRegExp);
0532     if (match < 0) {
0533         match = text.lastIndexOf(boundaryRegExp, -2);
0534     }
0535     boundaryStart.setColumn(m_document->computePositionWrtOffsets(decToEncOffsetList, qMax(0, match)));
0536     // and now the end position
0537     const int endLine = end.line();
0538     const int endColumn = end.column();
0539     if (endLine != startLine) {
0540         decToEncOffsetList.clear();
0541         encToDecOffsetList.clear();
0542         const KTextEditor::Range endLineRange(endLine, 0, endLine, m_document->lineLength(endLine));
0543         decodedLineText = m_document->decodeCharacters(endLineRange, decToEncOffsetList, encToDecOffsetList);
0544     }
0545     translatedColumn = m_document->computePositionWrtOffsets(encToDecOffsetList, endColumn);
0546     text = decodedLineText.mid(translatedColumn);
0547     boundaryEnd.setLine(endLine);
0548 
0549     QRegularExpressionMatch reMatch;
0550     match = text.indexOf(extendedBoundaryQuoteRegExp, 0 /* from */, &reMatch);
0551     if (match == 0) {
0552         match = reMatch.capturedLength(0);
0553     } else {
0554         match = text.indexOf(extendedBoundaryRegExp);
0555     }
0556     boundaryEnd.setColumn(m_document->computePositionWrtOffsets(decToEncOffsetList, translatedColumn + qMax(0, match)));
0557     return KTextEditor::Range(boundaryStart, boundaryEnd);
0558 }
0559 
0560 void KateOnTheFlyChecker::misspelling(const QString &word, int start)
0561 {
0562     if (m_currentlyCheckedItem == invalidSpellCheckQueueItem()) {
0563         ON_THE_FLY_DEBUG << "exited as no spell check is taking place";
0564         return;
0565     }
0566     int translatedStart = m_document->computePositionWrtOffsets(m_currentDecToEncOffsetList, start);
0567     //   ON_THE_FLY_DEBUG << "misspelled " << word
0568     //                                     << " at line "
0569     //                                     << *m_currentlyCheckedItem.first
0570     //                                     << " column " << start;
0571 
0572     KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first;
0573     int line = spellCheckRange->start().line();
0574     int rangeStart = spellCheckRange->start().column();
0575     int translatedEnd = m_document->computePositionWrtOffsets(m_currentDecToEncOffsetList, start + word.length());
0576 
0577     KTextEditor::MovingRange *movingRange =
0578         m_document->newMovingRange(KTextEditor::Range(line, rangeStart + translatedStart, line, rangeStart + translatedEnd));
0579     movingRange->setFeedback(this);
0580     KTextEditor::Attribute *attribute = new KTextEditor::Attribute();
0581     attribute->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
0582     attribute->setUnderlineColor(KateRendererConfig::global()->spellingMistakeLineColor());
0583 
0584     // don't print this range
0585     movingRange->setAttributeOnlyForViews(true);
0586 
0587     movingRange->setAttribute(KTextEditor::Attribute::Ptr(attribute));
0588     m_misspelledList.push_back(MisspelledItem(movingRange, m_currentlyCheckedItem.second));
0589 
0590     if (m_backgroundChecker) {
0591         m_backgroundChecker->continueChecking();
0592     }
0593 }
0594 
0595 void KateOnTheFlyChecker::spellCheckDone()
0596 {
0597     ON_THE_FLY_DEBUG << "on-the-fly spell check done, queue length " << m_spellCheckQueue.size();
0598     if (m_currentlyCheckedItem == invalidSpellCheckQueueItem()) {
0599         return;
0600     }
0601     KTextEditor::MovingRange *movingRange = m_currentlyCheckedItem.first;
0602     stopCurrentSpellCheck();
0603     deleteMovingRangeQuickly(movingRange);
0604 
0605     if (!m_spellCheckQueue.empty()) {
0606         QTimer::singleShot(0, this, SLOT(performSpellCheck()));
0607     }
0608 }
0609 
0610 QList<KTextEditor::MovingRange *> KateOnTheFlyChecker::installedMovingRanges(KTextEditor::Range range) const
0611 {
0612     ON_THE_FLY_DEBUG << range;
0613     MovingRangeList toReturn;
0614 
0615     for (QList<SpellCheckItem>::const_iterator i = m_misspelledList.begin(); i != m_misspelledList.end(); ++i) {
0616         KTextEditor::MovingRange *movingRange = (*i).first;
0617         if (movingRange->overlaps(range)) {
0618             toReturn.push_back(movingRange);
0619         }
0620     }
0621     return toReturn;
0622 }
0623 
0624 void KateOnTheFlyChecker::updateConfig()
0625 {
0626     ON_THE_FLY_DEBUG;
0627     // m_speller.restore();
0628 }
0629 
0630 void KateOnTheFlyChecker::refreshSpellCheck(KTextEditor::Range range)
0631 {
0632     if (range.isValid()) {
0633         textInserted(m_document, range);
0634     } else {
0635         freeDocument();
0636         textInserted(m_document, m_document->documentRange());
0637     }
0638 }
0639 
0640 void KateOnTheFlyChecker::addView(KTextEditor::Document *document, KTextEditor::View *view)
0641 {
0642     Q_ASSERT(document == m_document);
0643     Q_UNUSED(document);
0644     ON_THE_FLY_DEBUG;
0645     auto *viewPrivate = static_cast<KTextEditor::ViewPrivate *>(view);
0646     connect(viewPrivate, &KTextEditor::ViewPrivate::destroyed, this, &KateOnTheFlyChecker::viewDestroyed);
0647     connect(viewPrivate, &KTextEditor::ViewPrivate::displayRangeChanged, this, &KateOnTheFlyChecker::restartViewRefreshTimer);
0648     updateInstalledMovingRanges(static_cast<KTextEditor::ViewPrivate *>(view));
0649 }
0650 
0651 void KateOnTheFlyChecker::viewDestroyed(QObject *obj)
0652 {
0653     ON_THE_FLY_DEBUG;
0654     KTextEditor::View *view = static_cast<KTextEditor::View *>(obj);
0655     m_displayRangeMap.remove(view);
0656 }
0657 
0658 void KateOnTheFlyChecker::removeView(KTextEditor::View *view)
0659 {
0660     ON_THE_FLY_DEBUG;
0661     m_displayRangeMap.remove(view);
0662 }
0663 
0664 void KateOnTheFlyChecker::updateInstalledMovingRanges(KTextEditor::ViewPrivate *view)
0665 {
0666     Q_ASSERT(m_document == view->document());
0667     ON_THE_FLY_DEBUG;
0668     KTextEditor::Range oldDisplayRange = m_displayRangeMap[view];
0669 
0670     KTextEditor::Range newDisplayRange = view->visibleRange();
0671     ON_THE_FLY_DEBUG << "new range: " << newDisplayRange;
0672     ON_THE_FLY_DEBUG << "old range: " << oldDisplayRange;
0673     QList<KTextEditor::MovingRange *> toDelete;
0674     for (const MisspelledItem &item : std::as_const(m_misspelledList)) {
0675         KTextEditor::MovingRange *movingRange = item.first;
0676         if (!movingRange->overlaps(newDisplayRange)) {
0677             bool stillVisible = false;
0678             const auto views = m_document->views();
0679             for (KTextEditor::View *it2 : views) {
0680                 KTextEditor::ViewPrivate *view2 = static_cast<KTextEditor::ViewPrivate *>(it2);
0681                 if (view != view2 && movingRange->overlaps(view2->visibleRange())) {
0682                     stillVisible = true;
0683                     break;
0684                 }
0685             }
0686             if (!stillVisible) {
0687                 toDelete.push_back(movingRange);
0688             }
0689         }
0690     }
0691     deleteMovingRanges(toDelete);
0692     m_displayRangeMap[view] = newDisplayRange;
0693     if (oldDisplayRange.isValid()) {
0694         bool emptyAtStart = m_spellCheckQueue.empty();
0695         for (int line = newDisplayRange.end().line(); line >= newDisplayRange.start().line(); --line) {
0696             if (!oldDisplayRange.containsLine(line)) {
0697                 bool visible = false;
0698                 const auto views = m_document->views();
0699                 for (KTextEditor::View *it2 : views) {
0700                     KTextEditor::ViewPrivate *view2 = static_cast<KTextEditor::ViewPrivate *>(it2);
0701                     if (view != view2 && view2->visibleRange().containsLine(line)) {
0702                         visible = true;
0703                         break;
0704                     }
0705                 }
0706                 if (!visible) {
0707                     queueLineSpellCheck(m_document, line);
0708                 }
0709             }
0710         }
0711         if (emptyAtStart && !m_spellCheckQueue.isEmpty()) {
0712             QTimer::singleShot(0, this, SLOT(performSpellCheck()));
0713         }
0714     }
0715 }
0716 
0717 void KateOnTheFlyChecker::queueSpellCheckVisibleRange(KTextEditor::Range range)
0718 {
0719     const QList<KTextEditor::View *> &viewList = m_document->views();
0720     for (QList<KTextEditor::View *>::const_iterator i = viewList.begin(); i != viewList.end(); ++i) {
0721         queueSpellCheckVisibleRange(static_cast<KTextEditor::ViewPrivate *>(*i), range);
0722     }
0723 }
0724 
0725 void KateOnTheFlyChecker::queueSpellCheckVisibleRange(KTextEditor::ViewPrivate *view, KTextEditor::Range range)
0726 {
0727     Q_ASSERT(m_document == view->doc());
0728     KTextEditor::Range visibleRange = view->visibleRange();
0729     KTextEditor::Range intersection = visibleRange.intersect(range);
0730     if (intersection.isEmpty()) {
0731         return;
0732     }
0733 
0734     // clear all the highlights that are currently present in the range that
0735     // is supposed to be checked, necessary due to highlighting
0736     const MovingRangeList highlightsList = installedMovingRanges(intersection);
0737     deleteMovingRanges(highlightsList);
0738 
0739     QList<QPair<KTextEditor::Range, QString>> spellCheckRanges =
0740         KTextEditor::EditorPrivate::self()->spellCheckManager()->spellCheckRanges(m_document, intersection, true);
0741     // we queue them up in reverse
0742     QListIterator<QPair<KTextEditor::Range, QString>> i(spellCheckRanges);
0743     i.toBack();
0744     while (i.hasPrevious()) {
0745         QPair<KTextEditor::Range, QString> p = i.previous();
0746         queueLineSpellCheck(p.first, p.second);
0747     }
0748 }
0749 
0750 void KateOnTheFlyChecker::queueLineSpellCheck(KTextEditor::DocumentPrivate *kateDocument, int line)
0751 {
0752     const KTextEditor::Range range = KTextEditor::Range(line, 0, line, kateDocument->lineLength(line));
0753     // clear all the highlights that are currently present in the range that
0754     // is supposed to be checked, necessary due to highlighting
0755 
0756     const MovingRangeList highlightsList = installedMovingRanges(range);
0757     deleteMovingRanges(highlightsList);
0758 
0759     QList<QPair<KTextEditor::Range, QString>> spellCheckRanges =
0760         KTextEditor::EditorPrivate::self()->spellCheckManager()->spellCheckRanges(kateDocument, range, true);
0761     // we queue them up in reverse
0762     QListIterator<QPair<KTextEditor::Range, QString>> i(spellCheckRanges);
0763     i.toBack();
0764     while (i.hasPrevious()) {
0765         QPair<KTextEditor::Range, QString> p = i.previous();
0766         queueLineSpellCheck(p.first, p.second);
0767     }
0768 }
0769 
0770 void KateOnTheFlyChecker::queueLineSpellCheck(KTextEditor::Range range, const QString &dictionary)
0771 {
0772     ON_THE_FLY_DEBUG << m_document << range;
0773 
0774     Q_ASSERT(range.onSingleLine());
0775 
0776     if (range.isEmpty()) {
0777         return;
0778     }
0779 
0780     addToSpellCheckQueue(range, dictionary);
0781 }
0782 
0783 void KateOnTheFlyChecker::addToSpellCheckQueue(KTextEditor::Range range, const QString &dictionary)
0784 {
0785     addToSpellCheckQueue(m_document->newMovingRange(range), dictionary);
0786 }
0787 
0788 void KateOnTheFlyChecker::addToSpellCheckQueue(KTextEditor::MovingRange *range, const QString &dictionary)
0789 {
0790     ON_THE_FLY_DEBUG << m_document << *range << dictionary;
0791 
0792     range->setFeedback(this);
0793 
0794     // if the queue contains a subrange of 'range', we remove that one
0795     for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) {
0796         KTextEditor::MovingRange *spellCheckRange = (*i).first;
0797         if (range->contains(*spellCheckRange)) {
0798             deleteMovingRangeQuickly(spellCheckRange);
0799             i = m_spellCheckQueue.erase(i);
0800         } else {
0801             ++i;
0802         }
0803     }
0804     // leave 'push_front' here as it is a LIFO queue, i.e. a stack
0805     m_spellCheckQueue.push_front(SpellCheckItem(range, dictionary));
0806     ON_THE_FLY_DEBUG << "added" << *range << dictionary << "to the queue, which has a length of" << m_spellCheckQueue.size();
0807 }
0808 
0809 void KateOnTheFlyChecker::viewRefreshTimeout()
0810 {
0811     if (m_refreshView) {
0812         updateInstalledMovingRanges(m_refreshView);
0813     }
0814     m_refreshView = nullptr;
0815 }
0816 
0817 void KateOnTheFlyChecker::restartViewRefreshTimer(KTextEditor::ViewPrivate *view)
0818 {
0819     if (m_refreshView && view != m_refreshView) { // a new view should be refreshed
0820         updateInstalledMovingRanges(m_refreshView); // so refresh the old one first
0821     }
0822     m_refreshView = view;
0823     m_viewRefreshTimer->start(100);
0824 }
0825 
0826 void KateOnTheFlyChecker::deleteMovingRangeQuickly(KTextEditor::MovingRange *range)
0827 {
0828     range->setFeedback(nullptr);
0829     const auto views = m_document->views();
0830     for (KTextEditor::View *view : views) {
0831         static_cast<KTextEditor::ViewPrivate *>(view)->spellingMenu()->rangeDeleted(range);
0832     }
0833     delete (range);
0834 }
0835 
0836 void KateOnTheFlyChecker::handleModifiedRanges()
0837 {
0838     for (const ModificationItem &item : std::as_const(m_modificationList)) {
0839         KTextEditor::MovingRange *movingRange = item.second;
0840         KTextEditor::Range range = *movingRange;
0841         deleteMovingRangeQuickly(movingRange);
0842         if (item.first == TEXT_INSERTED) {
0843             handleInsertedText(range);
0844         } else {
0845             handleRemovedText(range);
0846         }
0847     }
0848     m_modificationList.clear();
0849 }
0850 
0851 bool KateOnTheFlyChecker::removeRangeFromModificationList(KTextEditor::MovingRange *range)
0852 {
0853     bool found = false;
0854     for (ModificationList::iterator i = m_modificationList.begin(); i != m_modificationList.end();) {
0855         ModificationItem item = *i;
0856         KTextEditor::MovingRange *movingRange = item.second;
0857         if (movingRange == range) {
0858             found = true;
0859             i = m_modificationList.erase(i);
0860         } else {
0861             ++i;
0862         }
0863     }
0864     return found;
0865 }
0866 
0867 void KateOnTheFlyChecker::clearModificationList()
0868 {
0869     for (const ModificationItem &item : std::as_const(m_modificationList)) {
0870         KTextEditor::MovingRange *movingRange = item.second;
0871         deleteMovingRangeQuickly(movingRange);
0872     }
0873     m_modificationList.clear();
0874 }
0875 
0876 #include "moc_ontheflycheck.cpp"