File indexing completed on 2024-04-14 03:55:23

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