| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2008-2010 Michel Ludwig <michel.ludwig@kdemail.net> |
| 3 | SPDX-FileCopyrightText: 2009 Joseph Wenninger <jowenn@kde.org> |
| 4 | |
| 5 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 6 | */ |
| 7 | #include "ontheflycheck.h" |
| 8 | |
| 9 | #include <QRegularExpression> |
| 10 | #include <QTimer> |
| 11 | |
| 12 | #include "katebuffer.h" |
| 13 | #include "kateconfig.h" |
| 14 | #include "kateglobal.h" |
| 15 | #include "katepartdebug.h" |
| 16 | #include "kateview.h" |
| 17 | #include "spellcheck.h" |
| 18 | #include "spellingmenu.h" |
| 19 | |
| 20 | #define ON_THE_FLY_DEBUG qCDebug(LOG_KTE) |
| 21 | |
| 22 | namespace |
| 23 | { |
| 24 | inline const QPair<KTextEditor::MovingRange *, QString> &invalidSpellCheckQueueItem() |
| 25 | { |
| 26 | static const auto item = QPair<KTextEditor::MovingRange *, QString>(nullptr, QString()); |
| 27 | return item; |
| 28 | } |
| 29 | |
| 30 | } |
| 31 | |
| 32 | KateOnTheFlyChecker::KateOnTheFlyChecker(KTextEditor::DocumentPrivate *document) |
| 33 | : QObject(document) |
| 34 | , m_document(document) |
| 35 | , m_backgroundChecker(nullptr) |
| 36 | , m_currentlyCheckedItem(invalidSpellCheckQueueItem()) |
| 37 | , m_refreshView(nullptr) |
| 38 | { |
| 39 | ON_THE_FLY_DEBUG << "created" ; |
| 40 | |
| 41 | m_viewRefreshTimer = new QTimer(this); |
| 42 | m_viewRefreshTimer->setSingleShot(true); |
| 43 | connect(sender: m_viewRefreshTimer, signal: &QTimer::timeout, context: this, slot: &KateOnTheFlyChecker::viewRefreshTimeout); |
| 44 | |
| 45 | connect(sender: document, signal: &KTextEditor::DocumentPrivate::textInsertedRange, context: this, slot: &KateOnTheFlyChecker::textInserted); |
| 46 | connect(sender: document, signal: &KTextEditor::DocumentPrivate::textRemoved, context: this, slot: &KateOnTheFlyChecker::textRemoved); |
| 47 | connect(sender: document, signal: &KTextEditor::DocumentPrivate::viewCreated, context: this, slot: &KateOnTheFlyChecker::addView); |
| 48 | connect(sender: document, signal: &KTextEditor::DocumentPrivate::highlightingModeChanged, context: this, slot: &KateOnTheFlyChecker::updateConfig); |
| 49 | connect(sender: &document->buffer(), signal: &KateBuffer::respellCheckBlock, context: this, slot: &KateOnTheFlyChecker::handleRespellCheckBlock); |
| 50 | |
| 51 | connect(sender: document, signal: &KTextEditor::Document::reloaded, context: this, slot: [this](KTextEditor::Document *) { |
| 52 | refreshSpellCheck(); |
| 53 | }); |
| 54 | |
| 55 | // load the settings for the speller |
| 56 | updateConfig(); |
| 57 | |
| 58 | const auto views = document->views(); |
| 59 | for (KTextEditor::View *view : views) { |
| 60 | addView(document, view); |
| 61 | } |
| 62 | refreshSpellCheck(); |
| 63 | } |
| 64 | |
| 65 | KateOnTheFlyChecker::~KateOnTheFlyChecker() |
| 66 | { |
| 67 | freeDocument(); |
| 68 | } |
| 69 | |
| 70 | QPair<KTextEditor::Range, QString> KateOnTheFlyChecker::getMisspelledItem(const KTextEditor::Cursor cursor) const |
| 71 | { |
| 72 | for (const MisspelledItem &item : m_misspelledList) { |
| 73 | KTextEditor::MovingRange *movingRange = item.first; |
| 74 | if (movingRange->contains(cursor)) { |
| 75 | return QPair<KTextEditor::Range, QString>(*movingRange, item.second); |
| 76 | } |
| 77 | } |
| 78 | return QPair<KTextEditor::Range, QString>(KTextEditor::Range::invalid(), QString()); |
| 79 | } |
| 80 | |
| 81 | QString KateOnTheFlyChecker::dictionaryForMisspelledRange(KTextEditor::Range range) const |
| 82 | { |
| 83 | for (const MisspelledItem &item : m_misspelledList) { |
| 84 | KTextEditor::MovingRange *movingRange = item.first; |
| 85 | if (*movingRange == range) { |
| 86 | return item.second; |
| 87 | } |
| 88 | } |
| 89 | return QString(); |
| 90 | } |
| 91 | |
| 92 | void KateOnTheFlyChecker::clearMisspellingForWord(const QString &word) |
| 93 | { |
| 94 | const MisspelledList misspelledList = m_misspelledList; // make a copy |
| 95 | for (const MisspelledItem &item : misspelledList) { |
| 96 | KTextEditor::MovingRange *movingRange = item.first; |
| 97 | if (m_document->text(range: *movingRange) == word) { |
| 98 | deleteMovingRange(range: movingRange); |
| 99 | } |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | void KateOnTheFlyChecker::handleRespellCheckBlock(int start, int end) |
| 104 | { |
| 105 | ON_THE_FLY_DEBUG << start << end; |
| 106 | KTextEditor::Range range(start, 0, end, m_document->lineLength(line: end)); |
| 107 | bool listEmpty = m_modificationList.isEmpty(); |
| 108 | KTextEditor::MovingRange *movingRange = m_document->newMovingRange(range); |
| 109 | movingRange->setFeedback(this); |
| 110 | // we don't handle this directly as the highlighting information might not be up-to-date yet |
| 111 | m_modificationList.push_back(t: ModificationItem(TEXT_INSERTED, movingRange)); |
| 112 | ON_THE_FLY_DEBUG << "added" << *movingRange; |
| 113 | if (listEmpty) { |
| 114 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::handleModifiedRanges); |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | void KateOnTheFlyChecker::textInserted(KTextEditor::Document *document, KTextEditor::Range range) |
| 119 | { |
| 120 | Q_ASSERT(document == m_document); |
| 121 | Q_UNUSED(document); |
| 122 | if (!range.isValid()) { |
| 123 | return; |
| 124 | } |
| 125 | |
| 126 | bool listEmptyAtStart = m_modificationList.isEmpty(); |
| 127 | |
| 128 | // don't consider a range that is not within the document range |
| 129 | const KTextEditor::Range documentIntersection = m_document->documentRange().intersect(range); |
| 130 | if (!documentIntersection.isValid()) { |
| 131 | return; |
| 132 | } |
| 133 | // for performance reasons we only want to schedule spellchecks for ranges that are visible |
| 134 | const auto views = m_document->views(); |
| 135 | for (KTextEditor::View *i : views) { |
| 136 | KTextEditor::ViewPrivate *view = static_cast<KTextEditor::ViewPrivate *>(i); |
| 137 | KTextEditor::Range visibleIntersection = documentIntersection.intersect(range: view->visibleRange()); |
| 138 | if (visibleIntersection.isValid()) { // allow empty intersections |
| 139 | // we don't handle this directly as the highlighting information might not be up-to-date yet |
| 140 | KTextEditor::MovingRange *movingRange = m_document->newMovingRange(range: visibleIntersection); |
| 141 | movingRange->setFeedback(this); |
| 142 | m_modificationList.push_back(t: ModificationItem(TEXT_INSERTED, movingRange)); |
| 143 | ON_THE_FLY_DEBUG << "added" << *movingRange; |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | if (listEmptyAtStart && !m_modificationList.isEmpty()) { |
| 148 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::handleModifiedRanges); |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | void KateOnTheFlyChecker::handleInsertedText(KTextEditor::Range range) |
| 153 | { |
| 154 | KTextEditor::Range consideredRange = range; |
| 155 | ON_THE_FLY_DEBUG << m_document << range; |
| 156 | |
| 157 | bool spellCheckInProgress = m_currentlyCheckedItem != invalidSpellCheckQueueItem(); |
| 158 | |
| 159 | if (spellCheckInProgress) { |
| 160 | KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first; |
| 161 | if (spellCheckRange->contains(range: consideredRange)) { |
| 162 | consideredRange = *spellCheckRange; |
| 163 | stopCurrentSpellCheck(); |
| 164 | deleteMovingRangeQuickly(range: spellCheckRange); |
| 165 | } else if (consideredRange.contains(range: *spellCheckRange)) { |
| 166 | stopCurrentSpellCheck(); |
| 167 | deleteMovingRangeQuickly(range: spellCheckRange); |
| 168 | } else if (consideredRange.overlaps(range: *spellCheckRange)) { |
| 169 | consideredRange.expandToRange(range: *spellCheckRange); |
| 170 | stopCurrentSpellCheck(); |
| 171 | deleteMovingRangeQuickly(range: spellCheckRange); |
| 172 | } else { |
| 173 | spellCheckInProgress = false; |
| 174 | } |
| 175 | } |
| 176 | for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { |
| 177 | KTextEditor::MovingRange *spellCheckRange = (*i).first; |
| 178 | if (spellCheckRange->contains(range: consideredRange)) { |
| 179 | consideredRange = *spellCheckRange; |
| 180 | ON_THE_FLY_DEBUG << "erasing range " << *i; |
| 181 | i = m_spellCheckQueue.erase(pos: i); |
| 182 | deleteMovingRangeQuickly(range: spellCheckRange); |
| 183 | } else if (consideredRange.contains(range: *spellCheckRange)) { |
| 184 | ON_THE_FLY_DEBUG << "erasing range " << *i; |
| 185 | i = m_spellCheckQueue.erase(pos: i); |
| 186 | deleteMovingRangeQuickly(range: spellCheckRange); |
| 187 | } else if (consideredRange.overlaps(range: *spellCheckRange)) { |
| 188 | consideredRange.expandToRange(range: *spellCheckRange); |
| 189 | ON_THE_FLY_DEBUG << "erasing range " << *i; |
| 190 | i = m_spellCheckQueue.erase(pos: i); |
| 191 | deleteMovingRangeQuickly(range: spellCheckRange); |
| 192 | } else { |
| 193 | ++i; |
| 194 | } |
| 195 | } |
| 196 | KTextEditor::Range spellCheckRange = findWordBoundaries(begin: consideredRange.start(), end: consideredRange.end()); |
| 197 | const bool emptyAtStart = m_spellCheckQueue.isEmpty(); |
| 198 | |
| 199 | queueSpellCheckVisibleRange(range: spellCheckRange); |
| 200 | |
| 201 | if (spellCheckInProgress || (emptyAtStart && !m_spellCheckQueue.isEmpty())) { |
| 202 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::performSpellCheck); |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | void KateOnTheFlyChecker::textRemoved(KTextEditor::Document *document, KTextEditor::Range range) |
| 207 | { |
| 208 | Q_ASSERT(document == m_document); |
| 209 | Q_UNUSED(document); |
| 210 | if (!range.isValid()) { |
| 211 | return; |
| 212 | } |
| 213 | |
| 214 | bool listEmptyAtStart = m_modificationList.isEmpty(); |
| 215 | |
| 216 | // don't consider a range that is behind the end of the document |
| 217 | const KTextEditor::Range documentIntersection = m_document->documentRange().intersect(range); |
| 218 | if (!documentIntersection.isValid()) { // the intersection might however be empty if the last |
| 219 | return; // word has been removed, for example |
| 220 | } |
| 221 | |
| 222 | // for performance reasons we only want to schedule spellchecks for ranges that are visible |
| 223 | const auto views = m_document->views(); |
| 224 | for (KTextEditor::View *i : views) { |
| 225 | KTextEditor::ViewPrivate *view = static_cast<KTextEditor::ViewPrivate *>(i); |
| 226 | KTextEditor::Range visibleIntersection = documentIntersection.intersect(range: view->visibleRange()); |
| 227 | if (visibleIntersection.isValid()) { // see above |
| 228 | // we don't handle this directly as the highlighting information might not be up-to-date yet |
| 229 | KTextEditor::MovingRange *movingRange = m_document->newMovingRange(range: visibleIntersection); |
| 230 | movingRange->setFeedback(this); |
| 231 | m_modificationList.push_back(t: ModificationItem(TEXT_REMOVED, movingRange)); |
| 232 | ON_THE_FLY_DEBUG << "added" << *movingRange << view->visibleRange(); |
| 233 | } |
| 234 | } |
| 235 | if (listEmptyAtStart && !m_modificationList.isEmpty()) { |
| 236 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::handleModifiedRanges); |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | inline bool rangesAdjacent(KTextEditor::Range r1, KTextEditor::Range r2) |
| 241 | { |
| 242 | return (r1.end() == r2.start()) || (r2.end() == r1.start()); |
| 243 | } |
| 244 | |
| 245 | void KateOnTheFlyChecker::handleRemovedText(KTextEditor::Range range) |
| 246 | { |
| 247 | ON_THE_FLY_DEBUG << range; |
| 248 | |
| 249 | QList<KTextEditor::Range> rangesToReCheck; |
| 250 | for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { |
| 251 | KTextEditor::MovingRange *spellCheckRange = (*i).first; |
| 252 | if (rangesAdjacent(r1: *spellCheckRange, r2: range) || spellCheckRange->contains(range)) { |
| 253 | ON_THE_FLY_DEBUG << "erasing range " << *i; |
| 254 | if (!spellCheckRange->isEmpty()) { |
| 255 | rangesToReCheck.push_back(t: *spellCheckRange); |
| 256 | } |
| 257 | deleteMovingRangeQuickly(range: spellCheckRange); |
| 258 | i = m_spellCheckQueue.erase(pos: i); |
| 259 | } else { |
| 260 | ++i; |
| 261 | } |
| 262 | } |
| 263 | bool spellCheckInProgress = m_currentlyCheckedItem != invalidSpellCheckQueueItem(); |
| 264 | const bool emptyAtStart = m_spellCheckQueue.isEmpty(); |
| 265 | if (spellCheckInProgress) { |
| 266 | KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first; |
| 267 | ON_THE_FLY_DEBUG << *spellCheckRange; |
| 268 | if (m_document->documentRange().contains(range: *spellCheckRange) && (rangesAdjacent(r1: *spellCheckRange, r2: range) || spellCheckRange->contains(range)) |
| 269 | && !spellCheckRange->isEmpty()) { |
| 270 | rangesToReCheck.push_back(t: *spellCheckRange); |
| 271 | ON_THE_FLY_DEBUG << "added the range " << *spellCheckRange; |
| 272 | stopCurrentSpellCheck(); |
| 273 | deleteMovingRangeQuickly(range: spellCheckRange); |
| 274 | } else if (spellCheckRange->isEmpty()) { |
| 275 | stopCurrentSpellCheck(); |
| 276 | deleteMovingRangeQuickly(range: spellCheckRange); |
| 277 | } else { |
| 278 | spellCheckInProgress = false; |
| 279 | } |
| 280 | } |
| 281 | for (QList<KTextEditor::Range>::iterator i = rangesToReCheck.begin(); i != rangesToReCheck.end(); ++i) { |
| 282 | queueSpellCheckVisibleRange(range: *i); |
| 283 | } |
| 284 | |
| 285 | KTextEditor::Range spellCheckRange = findWordBoundaries(begin: range.start(), end: range.start()); |
| 286 | KTextEditor::Cursor spellCheckEnd = spellCheckRange.end(); |
| 287 | |
| 288 | queueSpellCheckVisibleRange(range: spellCheckRange); |
| 289 | |
| 290 | if (range.numberOfLines() > 0) { |
| 291 | // FIXME: there is no currently no way of doing this better as we only get notifications for removals of |
| 292 | // of single lines, i.e. we don't know here how many lines have been removed in total |
| 293 | KTextEditor::Cursor nextLineStart(spellCheckEnd.line() + 1, 0); |
| 294 | const KTextEditor::Cursor documentEnd = m_document->documentEnd(); |
| 295 | if (nextLineStart < documentEnd) { |
| 296 | KTextEditor::Range rangeBelow = KTextEditor::Range(nextLineStart, documentEnd); |
| 297 | |
| 298 | const QList<KTextEditor::View *> &viewList = m_document->views(); |
| 299 | for (QList<KTextEditor::View *>::const_iterator i = viewList.begin(); i != viewList.end(); ++i) { |
| 300 | KTextEditor::ViewPrivate *view = static_cast<KTextEditor::ViewPrivate *>(*i); |
| 301 | const KTextEditor::Range visibleRange = view->visibleRange(); |
| 302 | KTextEditor::Range intersection = visibleRange.intersect(range: rangeBelow); |
| 303 | if (intersection.isValid()) { |
| 304 | queueSpellCheckVisibleRange(view, range: intersection); |
| 305 | } |
| 306 | } |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | ON_THE_FLY_DEBUG << "finished" ; |
| 311 | if (spellCheckInProgress || (emptyAtStart && !m_spellCheckQueue.isEmpty())) { |
| 312 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::performSpellCheck); |
| 313 | } |
| 314 | } |
| 315 | |
| 316 | void KateOnTheFlyChecker::freeDocument() |
| 317 | { |
| 318 | ON_THE_FLY_DEBUG; |
| 319 | |
| 320 | // empty the spell check queue |
| 321 | for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { |
| 322 | ON_THE_FLY_DEBUG << "erasing range " << *i; |
| 323 | KTextEditor::MovingRange *movingRange = (*i).first; |
| 324 | deleteMovingRangeQuickly(range: movingRange); |
| 325 | i = m_spellCheckQueue.erase(pos: i); |
| 326 | } |
| 327 | if (m_currentlyCheckedItem != invalidSpellCheckQueueItem()) { |
| 328 | KTextEditor::MovingRange *movingRange = m_currentlyCheckedItem.first; |
| 329 | deleteMovingRangeQuickly(range: movingRange); |
| 330 | } |
| 331 | stopCurrentSpellCheck(); |
| 332 | |
| 333 | const MisspelledList misspelledList = m_misspelledList; // make a copy! |
| 334 | for (const MisspelledItem &i : misspelledList) { |
| 335 | deleteMovingRange(range: i.first); |
| 336 | } |
| 337 | m_misspelledList.clear(); |
| 338 | clearModificationList(); |
| 339 | } |
| 340 | |
| 341 | void KateOnTheFlyChecker::performSpellCheck() |
| 342 | { |
| 343 | if (m_currentlyCheckedItem != invalidSpellCheckQueueItem()) { |
| 344 | ON_THE_FLY_DEBUG << "exited as a check is currently in progress" ; |
| 345 | return; |
| 346 | } |
| 347 | if (m_spellCheckQueue.isEmpty()) { |
| 348 | ON_THE_FLY_DEBUG << "exited as there is nothing to do" ; |
| 349 | return; |
| 350 | } |
| 351 | m_currentlyCheckedItem = m_spellCheckQueue.takeFirst(); |
| 352 | |
| 353 | KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first; |
| 354 | const QString &language = m_currentlyCheckedItem.second; |
| 355 | ON_THE_FLY_DEBUG << "for the range " << *spellCheckRange; |
| 356 | // clear all the highlights that are currently present in the range that |
| 357 | // is supposed to be checked |
| 358 | const MovingRangeList highlightsList = installedMovingRanges(range: *spellCheckRange); // make a copy! |
| 359 | deleteMovingRanges(list: highlightsList); |
| 360 | |
| 361 | m_currentDecToEncOffsetList.clear(); |
| 362 | KTextEditor::DocumentPrivate::OffsetList encToDecOffsetList; |
| 363 | QString text = m_document->decodeCharacters(range: *spellCheckRange, decToEncOffsetList&: m_currentDecToEncOffsetList, encToDecOffsetList); |
| 364 | ON_THE_FLY_DEBUG << "next spell checking" << text; |
| 365 | if (text.isEmpty()) { // passing an empty string to Sonnet can lead to a bad allocation exception |
| 366 | spellCheckDone(); // (bug 225867) |
| 367 | return; |
| 368 | } |
| 369 | if (m_speller.language() != language) { |
| 370 | m_speller.setLanguage(language); |
| 371 | } |
| 372 | if (!m_backgroundChecker) { |
| 373 | m_backgroundChecker = new Sonnet::BackgroundChecker(m_speller, this); |
| 374 | connect(sender: m_backgroundChecker, signal: &Sonnet::BackgroundChecker::misspelling, context: this, slot: &KateOnTheFlyChecker::misspelling); |
| 375 | connect(sender: m_backgroundChecker, signal: &Sonnet::BackgroundChecker::done, context: this, slot: &KateOnTheFlyChecker::spellCheckDone); |
| 376 | |
| 377 | KateSpellCheckManager *m_spellCheckManager = KTextEditor::EditorPrivate::self()->spellCheckManager(); |
| 378 | connect(sender: m_spellCheckManager, signal: &KateSpellCheckManager::wordAddedToDictionary, context: this, slot: &KateOnTheFlyChecker::addToDictionary); |
| 379 | connect(sender: m_spellCheckManager, signal: &KateSpellCheckManager::wordIgnored, context: this, slot: &KateOnTheFlyChecker::addToSession); |
| 380 | } |
| 381 | m_backgroundChecker->setSpeller(m_speller); |
| 382 | m_backgroundChecker->setText(text); // don't call 'start()' after this! |
| 383 | } |
| 384 | |
| 385 | void KateOnTheFlyChecker::addToDictionary(const QString &word) |
| 386 | { |
| 387 | if (m_backgroundChecker) { |
| 388 | m_backgroundChecker->addWordToPersonal(word); |
| 389 | } |
| 390 | } |
| 391 | |
| 392 | void KateOnTheFlyChecker::addToSession(const QString &word) |
| 393 | { |
| 394 | if (m_backgroundChecker) { |
| 395 | m_backgroundChecker->addWordToSession(word); |
| 396 | } |
| 397 | } |
| 398 | |
| 399 | void KateOnTheFlyChecker::removeRangeFromEverything(KTextEditor::MovingRange *movingRange) |
| 400 | { |
| 401 | Q_ASSERT(m_document == movingRange->document()); |
| 402 | ON_THE_FLY_DEBUG << *movingRange << "(" << movingRange << ")" ; |
| 403 | |
| 404 | if (removeRangeFromModificationList(range: movingRange)) { |
| 405 | return; // range was part of the modification queue, so we don't have |
| 406 | // to look further for it |
| 407 | } |
| 408 | |
| 409 | if (removeRangeFromSpellCheckQueue(range: movingRange)) { |
| 410 | return; // range was part of the spell check queue, so it cannot have been |
| 411 | // a misspelled range |
| 412 | } |
| 413 | |
| 414 | for (MisspelledList::iterator i = m_misspelledList.begin(); i != m_misspelledList.end();) { |
| 415 | if ((*i).first == movingRange) { |
| 416 | i = m_misspelledList.erase(pos: i); |
| 417 | } else { |
| 418 | ++i; |
| 419 | } |
| 420 | } |
| 421 | } |
| 422 | |
| 423 | bool KateOnTheFlyChecker::removeRangeFromCurrentSpellCheck(KTextEditor::MovingRange *range) |
| 424 | { |
| 425 | if (m_currentlyCheckedItem != invalidSpellCheckQueueItem() && m_currentlyCheckedItem.first == range) { |
| 426 | stopCurrentSpellCheck(); |
| 427 | return true; |
| 428 | } |
| 429 | return false; |
| 430 | } |
| 431 | |
| 432 | void KateOnTheFlyChecker::stopCurrentSpellCheck() |
| 433 | { |
| 434 | m_currentDecToEncOffsetList.clear(); |
| 435 | m_currentlyCheckedItem = invalidSpellCheckQueueItem(); |
| 436 | if (m_backgroundChecker) { |
| 437 | m_backgroundChecker->stop(); |
| 438 | } |
| 439 | } |
| 440 | |
| 441 | bool KateOnTheFlyChecker::removeRangeFromSpellCheckQueue(KTextEditor::MovingRange *range) |
| 442 | { |
| 443 | if (removeRangeFromCurrentSpellCheck(range)) { |
| 444 | if (!m_spellCheckQueue.isEmpty()) { |
| 445 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::performSpellCheck); |
| 446 | } |
| 447 | return true; |
| 448 | } |
| 449 | bool found = false; |
| 450 | for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { |
| 451 | if ((*i).first == range) { |
| 452 | i = m_spellCheckQueue.erase(pos: i); |
| 453 | found = true; |
| 454 | } else { |
| 455 | ++i; |
| 456 | } |
| 457 | } |
| 458 | return found; |
| 459 | } |
| 460 | |
| 461 | void KateOnTheFlyChecker::rangeEmpty(KTextEditor::MovingRange *range) |
| 462 | { |
| 463 | ON_THE_FLY_DEBUG << range->start() << range->end() << "(" << range << ")" ; |
| 464 | deleteMovingRange(range); |
| 465 | } |
| 466 | |
| 467 | void KateOnTheFlyChecker::rangeInvalid(KTextEditor::MovingRange *range) |
| 468 | { |
| 469 | ON_THE_FLY_DEBUG << range->start() << range->end() << "(" << range << ")" ; |
| 470 | deleteMovingRange(range); |
| 471 | } |
| 472 | |
| 473 | /** |
| 474 | * It is not enough to use 'caret/Entered/ExitedRange' only as the cursor doesn't move when some |
| 475 | * text has been selected. |
| 476 | **/ |
| 477 | void KateOnTheFlyChecker::caretEnteredRange(KTextEditor::MovingRange *range, KTextEditor::View *view) |
| 478 | { |
| 479 | KTextEditor::ViewPrivate *kateView = static_cast<KTextEditor::ViewPrivate *>(view); |
| 480 | kateView->spellingMenu()->caretEnteredMisspelledRange(range); |
| 481 | } |
| 482 | |
| 483 | void KateOnTheFlyChecker::caretExitedRange(KTextEditor::MovingRange *range, KTextEditor::View *view) |
| 484 | { |
| 485 | KTextEditor::ViewPrivate *kateView = static_cast<KTextEditor::ViewPrivate *>(view); |
| 486 | kateView->spellingMenu()->caretExitedMisspelledRange(range); |
| 487 | } |
| 488 | |
| 489 | void KateOnTheFlyChecker::deleteMovingRange(KTextEditor::MovingRange *range) |
| 490 | { |
| 491 | ON_THE_FLY_DEBUG << range; |
| 492 | // remove it from all our structures |
| 493 | removeRangeFromEverything(movingRange: range); |
| 494 | range->setFeedback(nullptr); |
| 495 | const auto views = m_document->views(); |
| 496 | for (KTextEditor::View *view : views) { |
| 497 | static_cast<KTextEditor::ViewPrivate *>(view)->spellingMenu()->rangeDeleted(range); |
| 498 | } |
| 499 | delete (range); |
| 500 | } |
| 501 | |
| 502 | void KateOnTheFlyChecker::deleteMovingRanges(const QList<KTextEditor::MovingRange *> &list) |
| 503 | { |
| 504 | for (KTextEditor::MovingRange *r : list) { |
| 505 | deleteMovingRange(range: r); |
| 506 | } |
| 507 | } |
| 508 | |
| 509 | KTextEditor::Range KateOnTheFlyChecker::findWordBoundaries(const KTextEditor::Cursor begin, const KTextEditor::Cursor end) |
| 510 | { |
| 511 | // FIXME: QTextBoundaryFinder should be ideally used for this, but it is currently |
| 512 | // still broken in Qt |
| 513 | static const QRegularExpression boundaryRegExp(QStringLiteral("\\b" ), QRegularExpression::UseUnicodePropertiesOption); |
| 514 | // handle spell checking of QLatin1String("isn't"), QLatin1String("doesn't"), etc. |
| 515 | static const QRegularExpression boundaryQuoteRegExp(QStringLiteral("\\b\\w+'\\w*$" ), QRegularExpression::UseUnicodePropertiesOption); |
| 516 | static const QRegularExpression extendedBoundaryRegExp(QStringLiteral("\\W|$" ), QRegularExpression::UseUnicodePropertiesOption); |
| 517 | static const QRegularExpression extendedBoundaryQuoteRegExp(QStringLiteral("^\\w*'\\w+\\b" ), QRegularExpression::UseUnicodePropertiesOption); // see above |
| 518 | KTextEditor::DocumentPrivate::OffsetList decToEncOffsetList; |
| 519 | KTextEditor::DocumentPrivate::OffsetList encToDecOffsetList; |
| 520 | const int startLine = begin.line(); |
| 521 | const int startColumn = begin.column(); |
| 522 | KTextEditor::Cursor boundaryStart; |
| 523 | KTextEditor::Cursor boundaryEnd; |
| 524 | // first we take care of the start position |
| 525 | const KTextEditor::Range startLineRange(startLine, 0, startLine, m_document->lineLength(line: startLine)); |
| 526 | QString decodedLineText = m_document->decodeCharacters(range: startLineRange, decToEncOffsetList, encToDecOffsetList); |
| 527 | int translatedColumn = m_document->computePositionWrtOffsets(offsetList: encToDecOffsetList, pos: startColumn); |
| 528 | QString text = decodedLineText.mid(position: 0, n: translatedColumn); |
| 529 | boundaryStart.setLine(startLine); |
| 530 | int match = text.lastIndexOf(re: boundaryQuoteRegExp); |
| 531 | if (match < 0) { |
| 532 | match = text.lastIndexOf(re: boundaryRegExp, from: -2); |
| 533 | } |
| 534 | boundaryStart.setColumn(m_document->computePositionWrtOffsets(offsetList: decToEncOffsetList, pos: qMax(a: 0, b: match))); |
| 535 | // and now the end position |
| 536 | const int endLine = end.line(); |
| 537 | const int endColumn = end.column(); |
| 538 | if (endLine != startLine) { |
| 539 | decToEncOffsetList.clear(); |
| 540 | encToDecOffsetList.clear(); |
| 541 | const KTextEditor::Range endLineRange(endLine, 0, endLine, m_document->lineLength(line: endLine)); |
| 542 | decodedLineText = m_document->decodeCharacters(range: endLineRange, decToEncOffsetList, encToDecOffsetList); |
| 543 | } |
| 544 | translatedColumn = m_document->computePositionWrtOffsets(offsetList: encToDecOffsetList, pos: endColumn); |
| 545 | text = decodedLineText.mid(position: translatedColumn); |
| 546 | boundaryEnd.setLine(endLine); |
| 547 | |
| 548 | QRegularExpressionMatch reMatch; |
| 549 | match = text.indexOf(re: extendedBoundaryQuoteRegExp, from: 0 /* from */, rmatch: &reMatch); |
| 550 | if (match == 0) { |
| 551 | match = reMatch.capturedLength(nth: 0); |
| 552 | } else { |
| 553 | match = text.indexOf(re: extendedBoundaryRegExp); |
| 554 | } |
| 555 | boundaryEnd.setColumn(m_document->computePositionWrtOffsets(offsetList: decToEncOffsetList, pos: translatedColumn + qMax(a: 0, b: match))); |
| 556 | return KTextEditor::Range(boundaryStart, boundaryEnd); |
| 557 | } |
| 558 | |
| 559 | void KateOnTheFlyChecker::misspelling(const QString &word, int start) |
| 560 | { |
| 561 | if (m_currentlyCheckedItem == invalidSpellCheckQueueItem()) { |
| 562 | ON_THE_FLY_DEBUG << "exited as no spell check is taking place" ; |
| 563 | return; |
| 564 | } |
| 565 | int translatedStart = m_document->computePositionWrtOffsets(offsetList: m_currentDecToEncOffsetList, pos: start); |
| 566 | // ON_THE_FLY_DEBUG << "misspelled " << word |
| 567 | // << " at line " |
| 568 | // << *m_currentlyCheckedItem.first |
| 569 | // << " column " << start; |
| 570 | |
| 571 | KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first; |
| 572 | int line = spellCheckRange->start().line(); |
| 573 | int rangeStart = spellCheckRange->start().column(); |
| 574 | int translatedEnd = m_document->computePositionWrtOffsets(offsetList: m_currentDecToEncOffsetList, pos: start + word.length()); |
| 575 | |
| 576 | KTextEditor::MovingRange *movingRange = |
| 577 | m_document->newMovingRange(range: KTextEditor::Range(line, rangeStart + translatedStart, line, rangeStart + translatedEnd)); |
| 578 | movingRange->setFeedback(this); |
| 579 | KTextEditor::Attribute *attribute = new KTextEditor::Attribute(); |
| 580 | attribute->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); |
| 581 | attribute->setUnderlineColor(KateRendererConfig::global()->spellingMistakeLineColor()); |
| 582 | |
| 583 | // don't print this range |
| 584 | movingRange->setAttributeOnlyForViews(true); |
| 585 | |
| 586 | movingRange->setAttribute(KTextEditor::Attribute::Ptr(attribute)); |
| 587 | m_misspelledList.push_back(t: MisspelledItem(movingRange, m_currentlyCheckedItem.second)); |
| 588 | |
| 589 | if (m_backgroundChecker) { |
| 590 | m_backgroundChecker->continueChecking(); |
| 591 | } |
| 592 | } |
| 593 | |
| 594 | void KateOnTheFlyChecker::spellCheckDone() |
| 595 | { |
| 596 | ON_THE_FLY_DEBUG << "on-the-fly spell check done, queue length " << m_spellCheckQueue.size(); |
| 597 | if (m_currentlyCheckedItem == invalidSpellCheckQueueItem()) { |
| 598 | return; |
| 599 | } |
| 600 | KTextEditor::MovingRange *movingRange = m_currentlyCheckedItem.first; |
| 601 | stopCurrentSpellCheck(); |
| 602 | deleteMovingRangeQuickly(range: movingRange); |
| 603 | |
| 604 | if (!m_spellCheckQueue.empty()) { |
| 605 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::performSpellCheck); |
| 606 | } |
| 607 | } |
| 608 | |
| 609 | QList<KTextEditor::MovingRange *> KateOnTheFlyChecker::installedMovingRanges(KTextEditor::Range range) const |
| 610 | { |
| 611 | ON_THE_FLY_DEBUG << range; |
| 612 | MovingRangeList toReturn; |
| 613 | |
| 614 | for (QList<SpellCheckItem>::const_iterator i = m_misspelledList.begin(); i != m_misspelledList.end(); ++i) { |
| 615 | KTextEditor::MovingRange *movingRange = (*i).first; |
| 616 | if (movingRange->overlaps(range)) { |
| 617 | toReturn.push_back(t: movingRange); |
| 618 | } |
| 619 | } |
| 620 | return toReturn; |
| 621 | } |
| 622 | |
| 623 | void KateOnTheFlyChecker::updateConfig() |
| 624 | { |
| 625 | ON_THE_FLY_DEBUG; |
| 626 | // m_speller.restore(); |
| 627 | } |
| 628 | |
| 629 | void KateOnTheFlyChecker::refreshSpellCheck(KTextEditor::Range range) |
| 630 | { |
| 631 | if (range.isValid()) { |
| 632 | textInserted(document: m_document, range); |
| 633 | } else { |
| 634 | freeDocument(); |
| 635 | textInserted(document: m_document, range: m_document->documentRange()); |
| 636 | } |
| 637 | } |
| 638 | |
| 639 | void KateOnTheFlyChecker::addView(KTextEditor::Document *document, KTextEditor::View *view) |
| 640 | { |
| 641 | Q_ASSERT(document == m_document); |
| 642 | Q_UNUSED(document); |
| 643 | ON_THE_FLY_DEBUG; |
| 644 | auto *viewPrivate = static_cast<KTextEditor::ViewPrivate *>(view); |
| 645 | connect(sender: viewPrivate, signal: &KTextEditor::ViewPrivate::destroyed, context: this, slot: &KateOnTheFlyChecker::viewDestroyed); |
| 646 | connect(sender: viewPrivate, signal: &KTextEditor::View::displayRangeChanged, context: this, slot: &KateOnTheFlyChecker::restartViewRefreshTimer); |
| 647 | updateInstalledMovingRanges(view: static_cast<KTextEditor::ViewPrivate *>(view)); |
| 648 | } |
| 649 | |
| 650 | void KateOnTheFlyChecker::viewDestroyed(QObject *obj) |
| 651 | { |
| 652 | ON_THE_FLY_DEBUG; |
| 653 | KTextEditor::View *view = static_cast<KTextEditor::View *>(obj); |
| 654 | m_displayRangeMap.erase(x: view); |
| 655 | } |
| 656 | |
| 657 | void KateOnTheFlyChecker::removeView(KTextEditor::View *view) |
| 658 | { |
| 659 | ON_THE_FLY_DEBUG; |
| 660 | m_displayRangeMap.erase(x: view); |
| 661 | } |
| 662 | |
| 663 | void KateOnTheFlyChecker::updateInstalledMovingRanges(KTextEditor::View *view) |
| 664 | { |
| 665 | Q_ASSERT(m_document == view->document()); |
| 666 | ON_THE_FLY_DEBUG; |
| 667 | KTextEditor::Range oldDisplayRange = m_displayRangeMap[view]; |
| 668 | |
| 669 | KTextEditor::Range newDisplayRange = static_cast<KTextEditor::ViewPrivate *>(view)->visibleRange(); |
| 670 | ON_THE_FLY_DEBUG << "new range: " << newDisplayRange; |
| 671 | ON_THE_FLY_DEBUG << "old range: " << oldDisplayRange; |
| 672 | QList<KTextEditor::MovingRange *> toDelete; |
| 673 | for (const MisspelledItem &item : std::as_const(t&: m_misspelledList)) { |
| 674 | KTextEditor::MovingRange *movingRange = item.first; |
| 675 | if (!movingRange->overlaps(range: newDisplayRange)) { |
| 676 | bool stillVisible = false; |
| 677 | const auto views = m_document->views(); |
| 678 | for (KTextEditor::View *it2 : views) { |
| 679 | KTextEditor::ViewPrivate *view2 = static_cast<KTextEditor::ViewPrivate *>(it2); |
| 680 | if (view != view2 && movingRange->overlaps(range: view2->visibleRange())) { |
| 681 | stillVisible = true; |
| 682 | break; |
| 683 | } |
| 684 | } |
| 685 | if (!stillVisible) { |
| 686 | toDelete.push_back(t: movingRange); |
| 687 | } |
| 688 | } |
| 689 | } |
| 690 | deleteMovingRanges(list: toDelete); |
| 691 | m_displayRangeMap[view] = newDisplayRange; |
| 692 | if (oldDisplayRange.isValid()) { |
| 693 | bool emptyAtStart = m_spellCheckQueue.empty(); |
| 694 | for (int line = newDisplayRange.end().line(); line >= newDisplayRange.start().line(); --line) { |
| 695 | if (!oldDisplayRange.containsLine(line)) { |
| 696 | bool visible = false; |
| 697 | const auto views = m_document->views(); |
| 698 | for (KTextEditor::View *it2 : views) { |
| 699 | KTextEditor::ViewPrivate *view2 = static_cast<KTextEditor::ViewPrivate *>(it2); |
| 700 | if (view != view2 && view2->visibleRange().containsLine(line)) { |
| 701 | visible = true; |
| 702 | break; |
| 703 | } |
| 704 | } |
| 705 | if (!visible) { |
| 706 | queueLineSpellCheck(document: m_document, line); |
| 707 | } |
| 708 | } |
| 709 | } |
| 710 | if (emptyAtStart && !m_spellCheckQueue.isEmpty()) { |
| 711 | QTimer::singleShot(interval: 0, receiver: this, slot: &KateOnTheFlyChecker::performSpellCheck); |
| 712 | } |
| 713 | } |
| 714 | } |
| 715 | |
| 716 | void KateOnTheFlyChecker::queueSpellCheckVisibleRange(KTextEditor::Range range) |
| 717 | { |
| 718 | const QList<KTextEditor::View *> &viewList = m_document->views(); |
| 719 | for (QList<KTextEditor::View *>::const_iterator i = viewList.begin(); i != viewList.end(); ++i) { |
| 720 | queueSpellCheckVisibleRange(view: static_cast<KTextEditor::ViewPrivate *>(*i), range); |
| 721 | } |
| 722 | } |
| 723 | |
| 724 | void KateOnTheFlyChecker::queueSpellCheckVisibleRange(KTextEditor::ViewPrivate *view, KTextEditor::Range range) |
| 725 | { |
| 726 | Q_ASSERT(m_document == view->doc()); |
| 727 | KTextEditor::Range visibleRange = view->visibleRange(); |
| 728 | KTextEditor::Range intersection = visibleRange.intersect(range); |
| 729 | if (intersection.isEmpty()) { |
| 730 | return; |
| 731 | } |
| 732 | |
| 733 | // clear all the highlights that are currently present in the range that |
| 734 | // is supposed to be checked, necessary due to highlighting |
| 735 | const MovingRangeList highlightsList = installedMovingRanges(range: intersection); |
| 736 | deleteMovingRanges(list: highlightsList); |
| 737 | |
| 738 | QList<QPair<KTextEditor::Range, QString>> spellCheckRanges = |
| 739 | KTextEditor::EditorPrivate::self()->spellCheckManager()->spellCheckRanges(doc: m_document, range: intersection, singleLine: true); |
| 740 | // we queue them up in reverse |
| 741 | QListIterator<QPair<KTextEditor::Range, QString>> i(spellCheckRanges); |
| 742 | i.toBack(); |
| 743 | while (i.hasPrevious()) { |
| 744 | QPair<KTextEditor::Range, QString> p = i.previous(); |
| 745 | queueLineSpellCheck(range: p.first, dictionary: p.second); |
| 746 | } |
| 747 | } |
| 748 | |
| 749 | void KateOnTheFlyChecker::queueLineSpellCheck(KTextEditor::DocumentPrivate *kateDocument, int line) |
| 750 | { |
| 751 | const KTextEditor::Range range = KTextEditor::Range(line, 0, line, kateDocument->lineLength(line)); |
| 752 | // clear all the highlights that are currently present in the range that |
| 753 | // is supposed to be checked, necessary due to highlighting |
| 754 | |
| 755 | const MovingRangeList highlightsList = installedMovingRanges(range); |
| 756 | deleteMovingRanges(list: highlightsList); |
| 757 | |
| 758 | QList<QPair<KTextEditor::Range, QString>> spellCheckRanges = |
| 759 | KTextEditor::EditorPrivate::self()->spellCheckManager()->spellCheckRanges(doc: kateDocument, range, singleLine: true); |
| 760 | // we queue them up in reverse |
| 761 | QListIterator<QPair<KTextEditor::Range, QString>> i(spellCheckRanges); |
| 762 | i.toBack(); |
| 763 | while (i.hasPrevious()) { |
| 764 | QPair<KTextEditor::Range, QString> p = i.previous(); |
| 765 | queueLineSpellCheck(range: p.first, dictionary: p.second); |
| 766 | } |
| 767 | } |
| 768 | |
| 769 | void KateOnTheFlyChecker::queueLineSpellCheck(KTextEditor::Range range, const QString &dictionary) |
| 770 | { |
| 771 | ON_THE_FLY_DEBUG << m_document << range; |
| 772 | |
| 773 | Q_ASSERT(range.onSingleLine()); |
| 774 | |
| 775 | if (range.isEmpty()) { |
| 776 | return; |
| 777 | } |
| 778 | |
| 779 | addToSpellCheckQueue(range, dictionary); |
| 780 | } |
| 781 | |
| 782 | void KateOnTheFlyChecker::addToSpellCheckQueue(KTextEditor::Range range, const QString &dictionary) |
| 783 | { |
| 784 | addToSpellCheckQueue(range: m_document->newMovingRange(range), dictionary); |
| 785 | } |
| 786 | |
| 787 | void KateOnTheFlyChecker::addToSpellCheckQueue(KTextEditor::MovingRange *range, const QString &dictionary) |
| 788 | { |
| 789 | ON_THE_FLY_DEBUG << m_document << *range << dictionary; |
| 790 | |
| 791 | range->setFeedback(this); |
| 792 | |
| 793 | // if the queue contains a subrange of 'range', we remove that one |
| 794 | for (QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { |
| 795 | KTextEditor::MovingRange *spellCheckRange = (*i).first; |
| 796 | if (range->contains(range: *spellCheckRange)) { |
| 797 | deleteMovingRangeQuickly(range: spellCheckRange); |
| 798 | i = m_spellCheckQueue.erase(pos: i); |
| 799 | } else { |
| 800 | ++i; |
| 801 | } |
| 802 | } |
| 803 | // leave 'push_front' here as it is a LIFO queue, i.e. a stack |
| 804 | m_spellCheckQueue.push_front(t: SpellCheckItem(range, dictionary)); |
| 805 | ON_THE_FLY_DEBUG << "added" << *range << dictionary << "to the queue, which has a length of" << m_spellCheckQueue.size(); |
| 806 | } |
| 807 | |
| 808 | void KateOnTheFlyChecker::viewRefreshTimeout() |
| 809 | { |
| 810 | if (m_refreshView) { |
| 811 | updateInstalledMovingRanges(view: m_refreshView); |
| 812 | } |
| 813 | m_refreshView = nullptr; |
| 814 | } |
| 815 | |
| 816 | void KateOnTheFlyChecker::restartViewRefreshTimer(KTextEditor::View *view) |
| 817 | { |
| 818 | if (m_refreshView && view != m_refreshView) { // a new view should be refreshed |
| 819 | updateInstalledMovingRanges(view: m_refreshView); // so refresh the old one first |
| 820 | } |
| 821 | m_refreshView = view; |
| 822 | m_viewRefreshTimer->start(msec: 100); |
| 823 | } |
| 824 | |
| 825 | void KateOnTheFlyChecker::deleteMovingRangeQuickly(KTextEditor::MovingRange *range) |
| 826 | { |
| 827 | range->setFeedback(nullptr); |
| 828 | const auto views = m_document->views(); |
| 829 | for (KTextEditor::View *view : views) { |
| 830 | static_cast<KTextEditor::ViewPrivate *>(view)->spellingMenu()->rangeDeleted(range); |
| 831 | } |
| 832 | delete (range); |
| 833 | } |
| 834 | |
| 835 | void KateOnTheFlyChecker::handleModifiedRanges() |
| 836 | { |
| 837 | for (const ModificationItem &item : std::as_const(t&: m_modificationList)) { |
| 838 | KTextEditor::MovingRange *movingRange = item.second; |
| 839 | KTextEditor::Range range = *movingRange; |
| 840 | deleteMovingRangeQuickly(range: movingRange); |
| 841 | if (item.first == TEXT_INSERTED) { |
| 842 | handleInsertedText(range); |
| 843 | } else { |
| 844 | handleRemovedText(range); |
| 845 | } |
| 846 | } |
| 847 | m_modificationList.clear(); |
| 848 | } |
| 849 | |
| 850 | bool KateOnTheFlyChecker::removeRangeFromModificationList(KTextEditor::MovingRange *range) |
| 851 | { |
| 852 | bool found = false; |
| 853 | for (ModificationList::iterator i = m_modificationList.begin(); i != m_modificationList.end();) { |
| 854 | ModificationItem item = *i; |
| 855 | KTextEditor::MovingRange *movingRange = item.second; |
| 856 | if (movingRange == range) { |
| 857 | found = true; |
| 858 | i = m_modificationList.erase(pos: i); |
| 859 | } else { |
| 860 | ++i; |
| 861 | } |
| 862 | } |
| 863 | return found; |
| 864 | } |
| 865 | |
| 866 | void KateOnTheFlyChecker::clearModificationList() |
| 867 | { |
| 868 | for (const ModificationItem &item : std::as_const(t&: m_modificationList)) { |
| 869 | KTextEditor::MovingRange *movingRange = item.second; |
| 870 | deleteMovingRangeQuickly(range: movingRange); |
| 871 | } |
| 872 | m_modificationList.clear(); |
| 873 | } |
| 874 | |