KTextEditor

ontheflycheck.cpp
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(m_viewRefreshTimer, &QTimer::timeout, this, &KateOnTheFlyChecker::viewRefreshTimeout);
44
45 connect(document, &KTextEditor::DocumentPrivate::textInsertedRange, this, &KateOnTheFlyChecker::textInserted);
46 connect(document, &KTextEditor::DocumentPrivate::textRemoved, this, &KateOnTheFlyChecker::textRemoved);
47 connect(document, &KTextEditor::DocumentPrivate::viewCreated, this, &KateOnTheFlyChecker::addView);
48 connect(document, &KTextEditor::DocumentPrivate::highlightingModeChanged, this, &KateOnTheFlyChecker::updateConfig);
49 connect(&document->buffer(), &KateBuffer::respellCheckBlock, this, &KateOnTheFlyChecker::handleRespellCheckBlock);
50
51 connect(document, &KTextEditor::Document::reloaded, this, [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 }
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(*movingRange) == word) {
98 deleteMovingRange(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(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(ModificationItem(TEXT_INSERTED, movingRange));
112 ON_THE_FLY_DEBUG << "added" << *movingRange;
113 if (listEmpty) {
114 QTimer::singleShot(0, this, &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(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(visibleIntersection);
141 movingRange->setFeedback(this);
142 m_modificationList.push_back(ModificationItem(TEXT_INSERTED, movingRange));
143 ON_THE_FLY_DEBUG << "added" << *movingRange;
144 }
145 }
146
147 if (listEmptyAtStart && !m_modificationList.isEmpty()) {
148 QTimer::singleShot(0, this, &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(consideredRange)) {
162 consideredRange = *spellCheckRange;
163 stopCurrentSpellCheck();
164 deleteMovingRangeQuickly(spellCheckRange);
165 } else if (consideredRange.contains(*spellCheckRange)) {
166 stopCurrentSpellCheck();
167 deleteMovingRangeQuickly(spellCheckRange);
168 } else if (consideredRange.overlaps(*spellCheckRange)) {
169 consideredRange.expandToRange(*spellCheckRange);
170 stopCurrentSpellCheck();
171 deleteMovingRangeQuickly(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(consideredRange)) {
179 consideredRange = *spellCheckRange;
180 ON_THE_FLY_DEBUG << "erasing range " << *i;
181 i = m_spellCheckQueue.erase(i);
182 deleteMovingRangeQuickly(spellCheckRange);
183 } else if (consideredRange.contains(*spellCheckRange)) {
184 ON_THE_FLY_DEBUG << "erasing range " << *i;
185 i = m_spellCheckQueue.erase(i);
186 deleteMovingRangeQuickly(spellCheckRange);
187 } else if (consideredRange.overlaps(*spellCheckRange)) {
188 consideredRange.expandToRange(*spellCheckRange);
189 ON_THE_FLY_DEBUG << "erasing range " << *i;
190 i = m_spellCheckQueue.erase(i);
191 deleteMovingRangeQuickly(spellCheckRange);
192 } else {
193 ++i;
194 }
195 }
196 KTextEditor::Range spellCheckRange = findWordBoundaries(consideredRange.start(), consideredRange.end());
197 const bool emptyAtStart = m_spellCheckQueue.isEmpty();
198
199 queueSpellCheckVisibleRange(spellCheckRange);
200
201 if (spellCheckInProgress || (emptyAtStart && !m_spellCheckQueue.isEmpty())) {
202 QTimer::singleShot(0, this, &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(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(visibleIntersection);
230 movingRange->setFeedback(this);
231 m_modificationList.push_back(ModificationItem(TEXT_REMOVED, movingRange));
232 ON_THE_FLY_DEBUG << "added" << *movingRange << view->visibleRange();
233 }
234 }
235 if (listEmptyAtStart && !m_modificationList.isEmpty()) {
236 QTimer::singleShot(0, this, &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(*spellCheckRange, range) || spellCheckRange->contains(range)) {
253 ON_THE_FLY_DEBUG << "erasing range " << *i;
254 if (!spellCheckRange->isEmpty()) {
255 rangesToReCheck.push_back(*spellCheckRange);
256 }
257 deleteMovingRangeQuickly(spellCheckRange);
258 i = m_spellCheckQueue.erase(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(*spellCheckRange) && (rangesAdjacent(*spellCheckRange, range) || spellCheckRange->contains(range))
269 && !spellCheckRange->isEmpty()) {
270 rangesToReCheck.push_back(*spellCheckRange);
271 ON_THE_FLY_DEBUG << "added the range " << *spellCheckRange;
272 stopCurrentSpellCheck();
273 deleteMovingRangeQuickly(spellCheckRange);
274 } else if (spellCheckRange->isEmpty()) {
275 stopCurrentSpellCheck();
276 deleteMovingRangeQuickly(spellCheckRange);
277 } else {
278 spellCheckInProgress = false;
279 }
280 }
281 for (QList<KTextEditor::Range>::iterator i = rangesToReCheck.begin(); i != rangesToReCheck.end(); ++i) {
282 queueSpellCheckVisibleRange(*i);
283 }
284
285 KTextEditor::Range spellCheckRange = findWordBoundaries(range.start(), range.start());
286 KTextEditor::Cursor spellCheckEnd = spellCheckRange.end();
287
288 queueSpellCheckVisibleRange(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(rangeBelow);
303 if (intersection.isValid()) {
304 queueSpellCheckVisibleRange(view, intersection);
305 }
306 }
307 }
308 }
309
310 ON_THE_FLY_DEBUG << "finished";
311 if (spellCheckInProgress || (emptyAtStart && !m_spellCheckQueue.isEmpty())) {
312 QTimer::singleShot(0, this, &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(movingRange);
325 i = m_spellCheckQueue.erase(i);
326 }
327 if (m_currentlyCheckedItem != invalidSpellCheckQueueItem()) {
328 KTextEditor::MovingRange *movingRange = m_currentlyCheckedItem.first;
329 deleteMovingRangeQuickly(movingRange);
330 }
331 stopCurrentSpellCheck();
332
333 const MisspelledList misspelledList = m_misspelledList; // make a copy!
334 for (const MisspelledItem &i : misspelledList) {
335 deleteMovingRange(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(*spellCheckRange); // make a copy!
359 deleteMovingRanges(highlightsList);
360
361 m_currentDecToEncOffsetList.clear();
362 KTextEditor::DocumentPrivate::OffsetList encToDecOffsetList;
363 QString text = m_document->decodeCharacters(*spellCheckRange, 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(m_backgroundChecker, &Sonnet::BackgroundChecker::misspelling, this, &KateOnTheFlyChecker::misspelling);
375 connect(m_backgroundChecker, &Sonnet::BackgroundChecker::done, this, &KateOnTheFlyChecker::spellCheckDone);
376
377 KateSpellCheckManager *m_spellCheckManager = KTextEditor::EditorPrivate::self()->spellCheckManager();
378 connect(m_spellCheckManager, &KateSpellCheckManager::wordAddedToDictionary, this, &KateOnTheFlyChecker::addToDictionary);
379 connect(m_spellCheckManager, &KateSpellCheckManager::wordIgnored, this, &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(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(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(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(0, this, &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(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(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(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(startLine));
526 QString decodedLineText = m_document->decodeCharacters(startLineRange, decToEncOffsetList, encToDecOffsetList);
527 int translatedColumn = m_document->computePositionWrtOffsets(encToDecOffsetList, startColumn);
528 QString text = decodedLineText.mid(0, translatedColumn);
529 boundaryStart.setLine(startLine);
530 int match = text.lastIndexOf(boundaryQuoteRegExp);
531 if (match < 0) {
532 match = text.lastIndexOf(boundaryRegExp, -2);
533 }
534 boundaryStart.setColumn(m_document->computePositionWrtOffsets(decToEncOffsetList, qMax(0, 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(endLine));
542 decodedLineText = m_document->decodeCharacters(endLineRange, decToEncOffsetList, encToDecOffsetList);
543 }
544 translatedColumn = m_document->computePositionWrtOffsets(encToDecOffsetList, endColumn);
545 text = decodedLineText.mid(translatedColumn);
546 boundaryEnd.setLine(endLine);
547
549 match = text.indexOf(extendedBoundaryQuoteRegExp, 0 /* from */, &reMatch);
550 if (match == 0) {
551 match = reMatch.capturedLength(0);
552 } else {
553 match = text.indexOf(extendedBoundaryRegExp);
554 }
555 boundaryEnd.setColumn(m_document->computePositionWrtOffsets(decToEncOffsetList, translatedColumn + qMax(0, 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(m_currentDecToEncOffsetList, 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(m_currentDecToEncOffsetList, start + word.length());
575
576 KTextEditor::MovingRange *movingRange =
577 m_document->newMovingRange(KTextEditor::Range(line, rangeStart + translatedStart, line, rangeStart + translatedEnd));
578 movingRange->setFeedback(this);
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(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(movingRange);
603
604 if (!m_spellCheckQueue.empty()) {
605 QTimer::singleShot(0, this, &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(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(m_document, range);
633 } else {
634 freeDocument();
635 textInserted(m_document, 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(viewPrivate, &KTextEditor::ViewPrivate::destroyed, this, &KateOnTheFlyChecker::viewDestroyed);
646 connect(viewPrivate, &KTextEditor::ViewPrivate::displayRangeChanged, this, &KateOnTheFlyChecker::restartViewRefreshTimer);
647 updateInstalledMovingRanges(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(view);
655}
656
657void KateOnTheFlyChecker::removeView(KTextEditor::View *view)
658{
659 ON_THE_FLY_DEBUG;
660 m_displayRangeMap.erase(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;
673 for (const MisspelledItem &item : std::as_const(m_misspelledList)) {
674 KTextEditor::MovingRange *movingRange = item.first;
675 if (!movingRange->overlaps(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(view2->visibleRange())) {
681 stillVisible = true;
682 break;
683 }
684 }
685 if (!stillVisible) {
686 toDelete.push_back(movingRange);
687 }
688 }
689 }
690 deleteMovingRanges(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(m_document, line);
707 }
708 }
709 }
710 if (emptyAtStart && !m_spellCheckQueue.isEmpty()) {
711 QTimer::singleShot(0, this, &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(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(intersection);
736 deleteMovingRanges(highlightsList);
737
739 KTextEditor::EditorPrivate::self()->spellCheckManager()->spellCheckRanges(m_document, intersection, true);
740 // we queue them up in reverse
742 i.toBack();
743 while (i.hasPrevious()) {
744 QPair<KTextEditor::Range, QString> p = i.previous();
745 queueLineSpellCheck(p.first, 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(highlightsList);
757
759 KTextEditor::EditorPrivate::self()->spellCheckManager()->spellCheckRanges(kateDocument, range, true);
760 // we queue them up in reverse
762 i.toBack();
763 while (i.hasPrevious()) {
764 QPair<KTextEditor::Range, QString> p = i.previous();
765 queueLineSpellCheck(p.first, 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(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(*spellCheckRange)) {
797 deleteMovingRangeQuickly(spellCheckRange);
798 i = m_spellCheckQueue.erase(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(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(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(m_refreshView); // so refresh the old one first
820 }
821 m_refreshView = view;
822 m_viewRefreshTimer->start(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(m_modificationList)) {
838 KTextEditor::MovingRange *movingRange = item.second;
839 KTextEditor::Range range = *movingRange;
840 deleteMovingRangeQuickly(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(i);
859 } else {
860 ++i;
861 }
862 }
863 return found;
864}
865
866void KateOnTheFlyChecker::clearModificationList()
867{
868 for (const ModificationItem &item : std::as_const(m_modificationList)) {
869 KTextEditor::MovingRange *movingRange = item.second;
870 deleteMovingRangeQuickly(movingRange);
871 }
872 m_modificationList.clear();
873}
A class which provides customized text decorations.
Definition attribute.h:51
The Cursor represents a position in a Document.
Definition cursor.h:75
void setColumn(int column) noexcept
Set the cursor column to column.
Definition cursor.h:201
void setLine(int line) noexcept
Set the cursor line to line.
Definition cursor.h:183
constexpr int line() const noexcept
Retrieve the line on which this cursor is situated.
Definition cursor.h:174
A KParts derived class representing a text document.
Definition document.h:284
void reloaded(KTextEditor::Document *document)
Emitted after the current document was reloaded.
KateSpellCheckManager * spellCheckManager()
spell check manager
Definition kateglobal.h:304
static KTextEditor::EditorPrivate * self()
Kate Part Internal stuff ;)
virtual int column() const =0
Retrieve the column on which this cursor is situated.
virtual int line() const =0
Retrieve the line on which this cursor is situated.
A range that is bound to a specific Document, and maintains its position.
virtual void setAttribute(Attribute::Ptr attribute)=0
Sets the currently active attribute for this range.
virtual const MovingCursor & start() const =0
Retrieve start cursor of this range, read-only.
virtual void setAttributeOnlyForViews(bool onlyForViews)=0
Set if this range's attribute is only visible in views, not for example prints.
bool isEmpty() const
Returns true if this range contains no characters, ie.
virtual const MovingCursor & end() const =0
Retrieve end cursor of this range, read-only.
virtual void setFeedback(MovingRangeFeedback *feedback)=0
Sets the currently active MovingRangeFeedback for this range.
virtual Document * document() const =0
Gets the document to which this range is bound.
bool contains(const Range &range) const
Check whether the this range wholly encompasses range.
bool overlaps(const Range &range) const
Check whether the this range overlaps with range.
Definition movingapi.cpp:29
An object representing a section of text, from one Cursor to another.
constexpr Cursor end() const noexcept
Get the end position of this range.
constexpr Cursor start() const noexcept
Get the start position of this range.
constexpr bool isEmpty() const noexcept
Returns true if this range contains no characters, ie.
constexpr bool overlaps(Range range) const noexcept
Check whether the this range overlaps with range.
constexpr bool onSingleLine() const noexcept
Check whether this range is wholly contained within one line, ie.
static constexpr Range invalid() noexcept
Returns an invalid range.
constexpr bool isValid() const noexcept
Validity check.
constexpr bool containsLine(int line) const noexcept
Returns true if this range wholly encompasses line.
constexpr bool contains(Range range) const noexcept
Check whether the this range wholly encompasses range.
bool expandToRange(Range range) noexcept
Expand this range if necessary to contain range.
constexpr Range intersect(Range range) const noexcept
Intersects this range with another, returning the shared area of the two ranges.
constexpr int numberOfLines() const noexcept
Returns the number of lines separating the start() and end() positions.
A text widget with KXMLGUIClient that represents a Document.
Definition view.h:244
bool addWordToSession(const QString &word)
void setText(const QString &text)
void misspelling(const QString &word, int start)
virtual void continueChecking()
QString language() const
void setLanguage(const QString &lang)
Q_SCRIPTABLE Q_NOREPLY void start()
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
const QList< QKeySequence > & begin()
const QList< QKeySequence > & end()
QList::iterator begin()
void clear()
QList::iterator end()
QList::iterator erase(QList::iterator pos)
bool isEmpty() const const
void push_back(const T &value)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void destroyed(QObject *obj)
int capturedLength(int nth) const const
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
int length() const const
QString mid(int position, int n) const const
void setUnderlineColor(const QColor &color)
void setUnderlineStyle(QTextCharFormat::UnderlineStyle style)
void start(int msec)
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Feb 24 2024 20:00:58 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.