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"