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
22namespace
23{
24inline 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
32KateOnTheFlyChecker::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
65KateOnTheFlyChecker::~KateOnTheFlyChecker()
66{
67 freeDocument();
68}
69
70QPair<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
81QString 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
92void 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
103void 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
118void 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
152void 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
206void 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
240inline bool rangesAdjacent(KTextEditor::Range r1, KTextEditor::Range r2)
241{
242 return (r1.end() == r2.start()) || (r2.end() == r1.start());
243}
244
245void 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
316void 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
341void 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
385void KateOnTheFlyChecker::addToDictionary(const QString &word)
386{
387 if (m_backgroundChecker) {
388 m_backgroundChecker->addWordToPersonal(word);
389 }
390}
391
392void KateOnTheFlyChecker::addToSession(const QString &word)
393{
394 if (m_backgroundChecker) {
395 m_backgroundChecker->addWordToSession(word);
396 }
397}
398
399void 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
423bool 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
432void KateOnTheFlyChecker::stopCurrentSpellCheck()
433{
434 m_currentDecToEncOffsetList.clear();
435 m_currentlyCheckedItem = invalidSpellCheckQueueItem();
436 if (m_backgroundChecker) {
437 m_backgroundChecker->stop();
438 }
439}
440
441bool 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
461void KateOnTheFlyChecker::rangeEmpty(KTextEditor::MovingRange *range)
462{
463 ON_THE_FLY_DEBUG << range->start() << range->end() << "(" << range << ")";
464 deleteMovingRange(range);
465}
466
467void 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 **/
477void 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
483void 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
489void 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
502void KateOnTheFlyChecker::deleteMovingRanges(const QList<KTextEditor::MovingRange *> &list)
503{
504 for (KTextEditor::MovingRange *r : list) {
505 deleteMovingRange(range: r);
506 }
507}
508
509KTextEditor::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
559void 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
594void 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
609QList<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
623void KateOnTheFlyChecker::updateConfig()
624{
625 ON_THE_FLY_DEBUG;
626 // m_speller.restore();
627}
628
629void 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
639void 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::ViewPrivate::displayRangeChanged, context: this, slot: &KateOnTheFlyChecker::restartViewRefreshTimer);
647 updateInstalledMovingRanges(view: static_cast<KTextEditor::ViewPrivate *>(view));
648}
649
650void 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
657void KateOnTheFlyChecker::removeView(KTextEditor::View *view)
658{
659 ON_THE_FLY_DEBUG;
660 m_displayRangeMap.erase(x: view);
661}
662
663void KateOnTheFlyChecker::updateInstalledMovingRanges(KTextEditor::ViewPrivate *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 = 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
716void 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
724void 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
749void 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
769void 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
782void KateOnTheFlyChecker::addToSpellCheckQueue(KTextEditor::Range range, const QString &dictionary)
783{
784 addToSpellCheckQueue(range: m_document->newMovingRange(range), dictionary);
785}
786
787void 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
808void KateOnTheFlyChecker::viewRefreshTimeout()
809{
810 if (m_refreshView) {
811 updateInstalledMovingRanges(view: m_refreshView);
812 }
813 m_refreshView = nullptr;
814}
815
816void KateOnTheFlyChecker::restartViewRefreshTimer(KTextEditor::ViewPrivate *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
825void 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
835void 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
850bool 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
866void 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

source code of ktexteditor/src/spellcheck/ontheflycheck.cpp