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 }