1
/* This file is part of the KDE libraries and the Kate part.
3
* Copyright (C) 2008-2010 by Michel Ludwig <michel.ludwig@kdemail.net>
4
* Copyright (C) 2009 by Joseph Wenninger <jowenn@kde.org>
6
* This library is free software; you can redistribute it and/or
7
* modify it under the terms of the GNU Library General Public
8
* License as published by the Free Software Foundation; either
9
* version 2 of the License, or (at your option) any later version.
11
* This library is distributed in the hope that it will be useful,
12
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
* Library General Public License for more details.
16
* You should have received a copy of the GNU Library General Public License
17
* along with this library; see the file COPYING.LIB. If not, write to
18
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19
* Boston, MA 02110-1301, USA.
22
/* If ever threads should be used again, thread communication and
23
* synchronization ought to be done with blocking queued signal connections.
26
#include "ontheflycheck.h"
30
#include "kateconfig.h"
31
#include "kateglobal.h"
32
#include "katerenderer.h"
34
#include "spellcheck.h"
35
#include "spellingmenu.h"
37
#define ON_THE_FLY_DEBUG kDebug(debugArea())
39
KateOnTheFlyChecker::KateOnTheFlyChecker(KateDocument *document)
42
m_backgroundChecker(NULL),
43
m_currentlyCheckedItem(invalidSpellCheckQueueItem),
46
ON_THE_FLY_DEBUG << "created";
48
m_viewRefreshTimer = new QTimer(this);
49
m_viewRefreshTimer->setSingleShot(true);
50
connect(m_viewRefreshTimer, SIGNAL(timeout()), this, SLOT(viewRefreshTimeout()));
52
connect(document, SIGNAL(textInserted(KTextEditor::Document*, const KTextEditor::Range&)),
53
this, SLOT(textInserted(KTextEditor::Document*, const KTextEditor::Range&)));
54
connect(document, SIGNAL(textRemoved(KTextEditor::Document*, const KTextEditor::Range&)),
55
this, SLOT(textRemoved(KTextEditor::Document*, const KTextEditor::Range&)));
56
connect(document, SIGNAL(viewCreated(KTextEditor::Document*, KTextEditor::View*)),
57
this, SLOT(addView(KTextEditor::Document*, KTextEditor::View*)));
58
connect(document, SIGNAL(highlightingModeChanged (KTextEditor::Document*)),
59
this, SLOT(updateConfig()));
60
connect(document, SIGNAL(respellCheckBlock(KateDocument*, int, int)),
61
this, SLOT(handleRespellCheckBlock(KateDocument*, int, int)));
63
// load the settings for the speller
66
foreach(KTextEditor::View* view, document->views()) {
67
addView(document, view);
72
KateOnTheFlyChecker::~KateOnTheFlyChecker()
77
int KateOnTheFlyChecker::debugArea()
79
static int s_area = KDebug::registerArea("Kate (On-The-Fly Spellcheck)");
83
QPair<KTextEditor::Range, QString> KateOnTheFlyChecker::getMisspelledItem(const KTextEditor::Cursor &cursor) const
85
foreach(const MisspelledItem &item, m_misspelledList) {
86
KTextEditor::MovingRange *movingRange = item.first;
87
if(movingRange->contains(cursor)) {
88
return QPair<KTextEditor::Range, QString>(*movingRange, item.second);
91
return QPair<KTextEditor::Range, QString>(KTextEditor::Range::invalid(), QString());
94
QString KateOnTheFlyChecker::dictionaryForMisspelledRange(const KTextEditor::Range& range) const
96
foreach(const MisspelledItem &item, m_misspelledList) {
97
KTextEditor::MovingRange *movingRange = item.first;
98
if(*movingRange == range) {
105
void KateOnTheFlyChecker::clearMisspellingForWord(const QString& word)
107
MisspelledList misspelledList = m_misspelledList; // make a copy
108
foreach(const MisspelledItem &item, misspelledList) {
109
KTextEditor::MovingRange *movingRange = item.first;
110
if(m_document->text(*movingRange) == word) {
111
deleteMovingRange(movingRange);
116
const KateOnTheFlyChecker::SpellCheckItem KateOnTheFlyChecker::invalidSpellCheckQueueItem =
117
SpellCheckItem(NULL, "");
119
void KateOnTheFlyChecker::handleRespellCheckBlock(KateDocument *kateDocument, int start, int end)
121
Q_ASSERT(kateDocument == m_document);
122
Q_UNUSED(kateDocument);
124
ON_THE_FLY_DEBUG << start << end;
125
KTextEditor::Range range(start, 0, end, m_document->lineLength(end));
126
bool listEmpty = m_modificationList.isEmpty();
127
KTextEditor::MovingRange *movingRange = m_document->newMovingRange(range);
128
movingRange->setFeedback(this);
129
// we don't handle this directly as the highlighting information might not be up-to-date yet
130
m_modificationList.push_back(ModificationItem(TEXT_INSERTED, movingRange));
131
ON_THE_FLY_DEBUG << "added" << *movingRange;
133
QTimer::singleShot(0, this, SLOT(handleModifiedRanges()));
137
void KateOnTheFlyChecker::textInserted(KTextEditor::Document *document, const KTextEditor::Range &range)
139
Q_ASSERT(document == m_document);
141
if(!range.isValid()) {
145
bool listEmptyAtStart = m_modificationList.isEmpty();
147
// don't consider a range that is not within the document range
148
const KTextEditor::Range documentIntersection = m_document->documentRange().intersect(range);
149
if(!documentIntersection.isValid()) {
152
// for performance reasons we only want to schedule spellchecks for ranges that are visible
153
foreach(KTextEditor::View* i, m_document->views()) {
154
KateView *view = static_cast<KateView*>(i);
155
KTextEditor::Range visibleIntersection = documentIntersection.intersect(view->visibleRange());
156
if(visibleIntersection.isValid()) { // allow empty intersections
157
// we don't handle this directly as the highlighting information might not be up-to-date yet
158
KTextEditor::MovingRange *movingRange = m_document->newMovingRange(visibleIntersection);
159
movingRange->setFeedback(this);
160
m_modificationList.push_back(ModificationItem(TEXT_INSERTED, movingRange));
161
ON_THE_FLY_DEBUG << "added" << *movingRange;
165
if(listEmptyAtStart && !m_modificationList.isEmpty()) {
166
QTimer::singleShot(0, this, SLOT(handleModifiedRanges()));
170
void KateOnTheFlyChecker::handleInsertedText(const KTextEditor::Range &range)
172
KTextEditor::Range consideredRange = range;
173
ON_THE_FLY_DEBUG << m_document << range;
175
bool spellCheckInProgress = m_currentlyCheckedItem != invalidSpellCheckQueueItem;
177
if(spellCheckInProgress) {
178
KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first;
179
if(spellCheckRange->contains(consideredRange)) {
180
consideredRange = *spellCheckRange;
181
stopCurrentSpellCheck();
182
deleteMovingRangeQuickly(spellCheckRange);
184
else if(consideredRange.contains(*spellCheckRange)) {
185
stopCurrentSpellCheck();
186
deleteMovingRangeQuickly(spellCheckRange);
188
else if(consideredRange.overlaps(*spellCheckRange)) {
189
consideredRange.expandToRange(*spellCheckRange);
190
stopCurrentSpellCheck();
191
deleteMovingRangeQuickly(spellCheckRange);
194
spellCheckInProgress = false;
197
for(QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin();
198
i != m_spellCheckQueue.end();) {
199
KTextEditor::MovingRange *spellCheckRange = (*i).first;
200
if(spellCheckRange->contains(consideredRange)) {
201
consideredRange = *spellCheckRange;
202
ON_THE_FLY_DEBUG << "erasing range " << *i;
203
i = m_spellCheckQueue.erase(i);
204
deleteMovingRangeQuickly(spellCheckRange);
206
else if(consideredRange.contains(*spellCheckRange)) {
207
ON_THE_FLY_DEBUG << "erasing range " << *i;
208
i = m_spellCheckQueue.erase(i);
209
deleteMovingRangeQuickly(spellCheckRange);
211
else if(consideredRange.overlaps(*spellCheckRange)) {
212
consideredRange.expandToRange(*spellCheckRange);
213
ON_THE_FLY_DEBUG << "erasing range " << *i;
214
i = m_spellCheckQueue.erase(i);
215
deleteMovingRangeQuickly(spellCheckRange);
221
KTextEditor::Range spellCheckRange = findWordBoundaries(consideredRange.start(),
222
consideredRange.end());
223
const bool emptyAtStart = m_spellCheckQueue.isEmpty();
225
queueSpellCheckVisibleRange(spellCheckRange);
227
if(spellCheckInProgress || (emptyAtStart && !m_spellCheckQueue.isEmpty())) {
228
QTimer::singleShot(0, this, SLOT(performSpellCheck()));
232
void KateOnTheFlyChecker::textRemoved(KTextEditor::Document *document, const KTextEditor::Range &range)
234
Q_ASSERT(document == m_document);
236
if(!range.isValid()) {
240
bool listEmptyAtStart = m_modificationList.isEmpty();
242
// don't consider a range that is behind the end of the document
243
const KTextEditor::Range documentIntersection = m_document->documentRange().intersect(range);
244
if(!documentIntersection.isValid()) { // the intersection might however be empty if the last
245
return; // word has been removed, for example
248
// for performance reasons we only want to schedule spellchecks for ranges that are visible
249
foreach(KTextEditor::View *i, m_document->views()) {
250
KateView *view = static_cast<KateView*>(i);
251
KTextEditor::Range visibleIntersection = documentIntersection.intersect(view->visibleRange());
252
if(visibleIntersection.isValid()) { // see above
253
// we don't handle this directly as the highlighting information might not be up-to-date yet
254
KTextEditor::MovingRange *movingRange = m_document->newMovingRange(visibleIntersection);
255
movingRange->setFeedback(this);
256
m_modificationList.push_back(ModificationItem(TEXT_REMOVED, movingRange));
257
ON_THE_FLY_DEBUG << "added" << *movingRange << view->visibleRange();
260
if(listEmptyAtStart && !m_modificationList.isEmpty()) {
261
QTimer::singleShot(0, this, SLOT(handleModifiedRanges()));
265
inline bool rangesAdjacent(const KTextEditor::Range &r1, const KTextEditor::Range &r2)
267
return (r1.end() == r2.start()) || (r2.end() == r1.start());
270
void KateOnTheFlyChecker::handleRemovedText(const KTextEditor::Range &range)
273
ON_THE_FLY_DEBUG << range;
275
QList<KTextEditor::Range> rangesToReCheck;
276
for(QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin();
277
i != m_spellCheckQueue.end();) {
278
KTextEditor::MovingRange *spellCheckRange = (*i).first;
279
if(rangesAdjacent(*spellCheckRange, range) || spellCheckRange->contains(range)) {
280
ON_THE_FLY_DEBUG << "erasing range " << *i;
281
if(!spellCheckRange->isEmpty()) {
282
rangesToReCheck.push_back(*spellCheckRange);
284
deleteMovingRangeQuickly(spellCheckRange);
285
i = m_spellCheckQueue.erase(i);
291
bool spellCheckInProgress = m_currentlyCheckedItem != invalidSpellCheckQueueItem;
292
const bool emptyAtStart = m_spellCheckQueue.isEmpty();
293
if(spellCheckInProgress) {
294
KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first;
295
ON_THE_FLY_DEBUG << *spellCheckRange;
296
if(m_document->documentRange().contains(*spellCheckRange)
297
&& (rangesAdjacent(*spellCheckRange, range) || spellCheckRange->contains(range))
298
&& !spellCheckRange->isEmpty()) {
299
rangesToReCheck.push_back(*spellCheckRange);
300
ON_THE_FLY_DEBUG << "added the range " << *spellCheckRange;
301
stopCurrentSpellCheck();
302
deleteMovingRangeQuickly(spellCheckRange);
304
else if(spellCheckRange->isEmpty()) {
305
stopCurrentSpellCheck();
306
deleteMovingRangeQuickly(spellCheckRange);
309
spellCheckInProgress = false;
312
for(QList<KTextEditor::Range>::iterator i = rangesToReCheck.begin(); i != rangesToReCheck.end(); ++i) {
313
queueSpellCheckVisibleRange(*i);
316
KTextEditor::Range spellCheckRange = findWordBoundaries(range.start(), range.start());
317
KTextEditor::Cursor spellCheckEnd = spellCheckRange.end();
319
queueSpellCheckVisibleRange(spellCheckRange);
321
if(range.numberOfLines() > 0) {
322
//FIXME: there is no currently no way of doing this better as we only get notifications for removals of
323
// of single lines, i.e. we don't know here how many lines have been removed in total
324
KTextEditor::Cursor nextLineStart(spellCheckEnd.line() + 1, 0);
325
const KTextEditor::Cursor documentEnd = m_document->documentEnd();
326
if(nextLineStart < documentEnd) {
327
KTextEditor::Range rangeBelow = KTextEditor::Range(nextLineStart, documentEnd);
329
const QList<KTextEditor::View*>& viewList = m_document->views();
330
for(QList<KTextEditor::View*>::const_iterator i = viewList.begin(); i != viewList.end(); ++i) {
331
KateView *view = static_cast<KateView*>(*i);
332
const KTextEditor::Range visibleRange = view->visibleRange();
333
KTextEditor::Range intersection = visibleRange.intersect(rangeBelow);
334
if(intersection.isValid()) {
335
queueSpellCheckVisibleRange(view, intersection);
341
ON_THE_FLY_DEBUG << "finished";
342
if(spellCheckInProgress || (emptyAtStart && !m_spellCheckQueue.isEmpty())) {
343
QTimer::singleShot(0, this, SLOT(performSpellCheck()));
347
void KateOnTheFlyChecker::freeDocument()
351
// empty the spell check queue
352
for(QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin();
353
i != m_spellCheckQueue.end();) {
354
ON_THE_FLY_DEBUG << "erasing range " << *i;
355
KTextEditor::MovingRange *movingRange = (*i).first;
356
deleteMovingRangeQuickly(movingRange);
357
i = m_spellCheckQueue.erase(i);
359
if(m_currentlyCheckedItem != invalidSpellCheckQueueItem) {
360
KTextEditor::MovingRange *movingRange = m_currentlyCheckedItem.first;
361
deleteMovingRangeQuickly(movingRange);
363
stopCurrentSpellCheck();
365
MisspelledList misspelledList = m_misspelledList; // make a copy!
366
foreach(const MisspelledItem &i, misspelledList) {
367
deleteMovingRange(i.first);
369
m_misspelledList.clear();
370
clearModificationList();
373
void KateOnTheFlyChecker::performSpellCheck()
375
if(m_currentlyCheckedItem != invalidSpellCheckQueueItem) {
376
ON_THE_FLY_DEBUG << "exited as a check is currently in progress";
379
if(m_spellCheckQueue.isEmpty()) {
380
ON_THE_FLY_DEBUG << "exited as there is nothing to do";
383
m_currentlyCheckedItem = m_spellCheckQueue.takeFirst();
385
KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first;
386
const QString& language = m_currentlyCheckedItem.second;
387
ON_THE_FLY_DEBUG << "for the range " << *spellCheckRange;
388
// clear all the highlights that are currently present in the range that
389
// is supposed to be checked
390
const MovingRangeList highlightsList = installedMovingRanges(*spellCheckRange); // make a copy!
391
deleteMovingRanges(highlightsList);
393
m_currentDecToEncOffsetList.clear();
394
KateDocument::OffsetList encToDecOffsetList;
395
QString text = m_document->decodeCharacters(*spellCheckRange,
396
m_currentDecToEncOffsetList,
398
ON_THE_FLY_DEBUG << "next spell checking" << text;
399
if(text.isEmpty()) { // passing an empty string to Sonnet can lead to a bad allocation exception
400
spellCheckDone(); // (bug 225867)
403
if(m_speller.language() != language) {
404
m_speller.setLanguage(language);
406
if(!m_backgroundChecker) {
407
m_backgroundChecker = new Sonnet::BackgroundChecker(m_speller, this);
408
connect(m_backgroundChecker,
409
SIGNAL(misspelling(const QString&,int)),
411
SLOT(misspelling(const QString&,int)));
412
connect(m_backgroundChecker, SIGNAL(done()), this, SLOT(spellCheckDone()));
414
#if KDE_IS_VERSION(4,5,2)
415
// guard necessary to ensure compilation of KatePart's git repository against <= 4.5.1
416
m_backgroundChecker->restore(KGlobal::config().data());
419
m_backgroundChecker->setSpeller(m_speller);
420
m_backgroundChecker->setText(text); // don't call 'start()' after this!
423
void KateOnTheFlyChecker::removeRangeFromEverything(KTextEditor::MovingRange *movingRange)
425
Q_ASSERT(m_document == movingRange->document());
426
ON_THE_FLY_DEBUG << *movingRange << "(" << movingRange << ")";
428
if(removeRangeFromModificationList(movingRange)) {
429
return; // range was part of the modification queue, so we don't have
430
// to look further for it
433
if(removeRangeFromSpellCheckQueue(movingRange)) {
434
return; // range was part of the spell check queue, so it cannot have been
435
// a misspelled range
438
for(MisspelledList::iterator i = m_misspelledList.begin(); i != m_misspelledList.end();) {
439
if((*i).first == movingRange) {
440
i = m_misspelledList.erase(i);
448
bool KateOnTheFlyChecker::removeRangeFromCurrentSpellCheck(KTextEditor::MovingRange *range)
450
if(m_currentlyCheckedItem != invalidSpellCheckQueueItem
451
&& m_currentlyCheckedItem.first == range) {
452
stopCurrentSpellCheck();
458
void KateOnTheFlyChecker::stopCurrentSpellCheck()
460
m_currentDecToEncOffsetList.clear();
461
m_currentlyCheckedItem = invalidSpellCheckQueueItem;
462
if(m_backgroundChecker) {
463
m_backgroundChecker->stop();
467
bool KateOnTheFlyChecker::removeRangeFromSpellCheckQueue(KTextEditor::MovingRange *range)
469
if(removeRangeFromCurrentSpellCheck(range)) {
470
if(!m_spellCheckQueue.isEmpty()) {
471
QTimer::singleShot(0, this, SLOT(performSpellCheck()));
476
for(QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin();
477
i != m_spellCheckQueue.end();) {
478
if((*i).first == range) {
479
i = m_spellCheckQueue.erase(i);
489
void KateOnTheFlyChecker::rangeEmpty(KTextEditor::MovingRange *range)
491
ON_THE_FLY_DEBUG << range->start() << range->end() << "(" << range << ")";
492
deleteMovingRange (range);
495
void KateOnTheFlyChecker::rangeInvalid (KTextEditor::MovingRange* range)
497
ON_THE_FLY_DEBUG << range->start() << range->end() << "(" << range << ")";
498
deleteMovingRange (range);
501
void KateOnTheFlyChecker::mouseEnteredRange(KTextEditor::MovingRange *range, KTextEditor::View *view)
503
KateView *kateView = static_cast<KateView*>(view);
504
kateView->spellingMenu()->mouseEnteredMisspelledRange(range);
507
void KateOnTheFlyChecker::mouseExitedRange(KTextEditor::MovingRange *range, KTextEditor::View *view)
509
KateView *kateView = static_cast<KateView*>(view);
510
kateView->spellingMenu()->mouseExitedMisspelledRange(range);
514
* It is not enough to use 'caret/Entered/ExitedRange' only as the cursor doesn't move when some
515
* text has been selected.
517
void KateOnTheFlyChecker::caretEnteredRange(KTextEditor::MovingRange *range, KTextEditor::View *view)
519
KateView *kateView = static_cast<KateView*>(view);
520
kateView->spellingMenu()->caretEnteredMisspelledRange(range);
523
void KateOnTheFlyChecker::caretExitedRange(KTextEditor::MovingRange *range, KTextEditor::View *view)
525
KateView *kateView = static_cast<KateView*>(view);
526
kateView->spellingMenu()->caretExitedMisspelledRange(range);
529
void KateOnTheFlyChecker::deleteMovingRange(KTextEditor::MovingRange *range)
531
ON_THE_FLY_DEBUG << range;
532
// remove it from all our structures
533
removeRangeFromEverything(range);
534
range->setFeedback(NULL);
535
foreach(KTextEditor::View *view, m_document->views()) {
536
static_cast<KateView*>(view)->spellingMenu()->rangeDeleted(range);
541
void KateOnTheFlyChecker::deleteMovingRanges(const QList<KTextEditor::MovingRange*>& list)
543
foreach(KTextEditor::MovingRange *r, list) {
544
deleteMovingRange(r);
548
KTextEditor::Range KateOnTheFlyChecker::findWordBoundaries(const KTextEditor::Cursor& begin,
549
const KTextEditor::Cursor& end)
551
// FIXME: QTextBoundaryFinder should be ideally used for this, but it is currently
552
// still broken in Qt
553
const QRegExp boundaryRegExp("\\b");
554
const QRegExp boundaryQuoteRegExp("\\b\\w+'\\w*$"); // handle spell checking of "isn't", "doesn't", etc.
555
const QRegExp extendedBoundaryRegExp("(\\W|$)");
556
const QRegExp extendedBoundaryQuoteRegExp("^\\w*'\\w+\\b"); // see above
557
KateDocument::OffsetList decToEncOffsetList, encToDecOffsetList;
558
const int startLine = begin.line();
559
const int startColumn = begin.column();
560
KTextEditor::Cursor boundaryStart, boundaryEnd;
561
// first we take care of the start position
562
const KTextEditor::Range startLineRange(startLine, 0, startLine, m_document->lineLength(startLine));
563
QString decodedLineText = m_document->decodeCharacters(startLineRange,
566
int translatedColumn = m_document->computePositionWrtOffsets(encToDecOffsetList,
568
QString text = decodedLineText.mid(0, translatedColumn);
569
boundaryStart.setLine(startLine);
570
int match = text.lastIndexOf(boundaryQuoteRegExp);
572
match = text.lastIndexOf(boundaryRegExp);
574
boundaryStart.setColumn(m_document->computePositionWrtOffsets(decToEncOffsetList, qMax(0, match)));
575
// and now the end position
576
const int endLine = end.line();
577
const int endColumn = end.column();
578
if(endLine != startLine) {
579
decToEncOffsetList.clear();
580
encToDecOffsetList.clear();
581
const KTextEditor::Range endLineRange(endLine, 0, endLine, m_document->lineLength(endLine));
582
decodedLineText = m_document->decodeCharacters(endLineRange,
586
translatedColumn = m_document->computePositionWrtOffsets(encToDecOffsetList,
588
text = decodedLineText.mid(translatedColumn);
589
boundaryEnd.setLine(endLine);
590
match = extendedBoundaryQuoteRegExp.indexIn(text);
592
match = extendedBoundaryQuoteRegExp.matchedLength();
595
match = extendedBoundaryRegExp.indexIn(text);
597
boundaryEnd.setColumn(m_document->computePositionWrtOffsets(decToEncOffsetList,
598
translatedColumn + qMax(0, match)));
599
return KTextEditor::Range(boundaryStart, boundaryEnd);
602
void KateOnTheFlyChecker::misspelling(const QString &word, int start)
604
if(m_currentlyCheckedItem == invalidSpellCheckQueueItem) {
605
ON_THE_FLY_DEBUG << "exited as no spell check is taking place";
608
int translatedStart = m_document->computePositionWrtOffsets(m_currentDecToEncOffsetList,
610
// ON_THE_FLY_DEBUG << "misspelled " << word
612
// << *m_currentlyCheckedItem.first
613
// << " column " << start;
615
KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first;
616
int line = spellCheckRange->start().line();
617
int rangeStart = spellCheckRange->start().column();
618
int translatedEnd = m_document->computePositionWrtOffsets(m_currentDecToEncOffsetList,
619
start + word.length());
621
KTextEditor::MovingRange *movingRange =
622
m_document->newMovingRange(KTextEditor::Range(line,
623
rangeStart + translatedStart,
625
rangeStart + translatedEnd));
626
movingRange->setFeedback(this);
627
KTextEditor::Attribute *attribute = new KTextEditor::Attribute();
628
attribute->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
629
attribute->setUnderlineColor(KateRendererConfig::global()->spellingMistakeLineColor());
631
// don't print this range
632
movingRange->setAttributeOnlyForViews (true);
634
movingRange->setAttribute(KTextEditor::Attribute::Ptr(attribute));
635
m_misspelledList.push_back(MisspelledItem(movingRange, m_currentlyCheckedItem.second));
637
if(m_backgroundChecker) {
638
m_backgroundChecker->continueChecking();
642
void KateOnTheFlyChecker::spellCheckDone()
644
ON_THE_FLY_DEBUG << "on-the-fly spell check done, queue length " << m_spellCheckQueue.size();
645
if(m_currentlyCheckedItem == invalidSpellCheckQueueItem) {
648
KTextEditor::MovingRange *movingRange = m_currentlyCheckedItem.first;
649
stopCurrentSpellCheck();
650
deleteMovingRangeQuickly(movingRange);
652
if(!m_spellCheckQueue.empty()) {
653
QTimer::singleShot(0, this, SLOT(performSpellCheck()));
657
QList<KTextEditor::MovingRange*> KateOnTheFlyChecker::installedMovingRanges(const KTextEditor::Range& range)
659
ON_THE_FLY_DEBUG << range;
660
MovingRangeList toReturn;
662
for(QList<SpellCheckItem>::iterator i = m_misspelledList.begin();
663
i != m_misspelledList.end(); ++i) {
664
KTextEditor::MovingRange *movingRange = (*i).first;
665
if(movingRange->overlaps(range)) {
666
toReturn.push_back(movingRange);
672
void KateOnTheFlyChecker::updateConfig()
675
m_speller.restore(KGlobal::config().data());
677
#if KDE_IS_VERSION(4,5,2)
678
// guard necessary to ensure compilation of KatePart's git repository against <= 4.5.1
679
if(m_backgroundChecker) {
680
m_backgroundChecker->restore(KGlobal::config().data());
685
void KateOnTheFlyChecker::refreshSpellCheck(const KTextEditor::Range &range)
687
if(range.isValid()) {
688
textInserted(m_document, range);
692
textInserted(m_document, m_document->documentRange());
696
void KateOnTheFlyChecker::addView(KTextEditor::Document *document, KTextEditor::View *view)
698
Q_ASSERT(document == m_document);
701
connect(view, SIGNAL(destroyed(QObject*)), this, SLOT(viewDestroyed(QObject*)));
702
connect(view, SIGNAL(displayRangeChanged(KateView*)), this, SLOT(restartViewRefreshTimer(KateView*)));
703
updateInstalledMovingRanges(static_cast<KateView*>(view));
706
void KateOnTheFlyChecker::viewDestroyed(QObject* obj)
709
KTextEditor::View *view = static_cast<KTextEditor::View*>(obj);
710
m_displayRangeMap.remove(view);
713
void KateOnTheFlyChecker::removeView(KTextEditor::View *view)
716
m_displayRangeMap.remove(view);
719
void KateOnTheFlyChecker::updateInstalledMovingRanges(KateView *view)
721
Q_ASSERT(m_document == view->document());
723
KTextEditor::Range oldDisplayRange = m_displayRangeMap[view];
725
KTextEditor::Range newDisplayRange = view->visibleRange();
726
ON_THE_FLY_DEBUG << "new range: " << newDisplayRange;
727
ON_THE_FLY_DEBUG << "old range: " << oldDisplayRange;
728
QList<KTextEditor::MovingRange*> toDelete;
729
foreach(const MisspelledItem &item, m_misspelledList) {
730
KTextEditor::MovingRange *movingRange = item.first;
731
if(!movingRange->overlaps(newDisplayRange)) {
732
bool stillVisible = false;
733
foreach(KTextEditor::View *it2, m_document->views()) {
734
KateView *view2 = static_cast<KateView*>(it2);
735
if(view != view2 && movingRange->overlaps(view2->visibleRange())) {
741
toDelete.push_back(movingRange);
745
deleteMovingRanges (toDelete);
746
m_displayRangeMap[view] = newDisplayRange;
747
if(oldDisplayRange.isValid()) {
748
bool emptyAtStart = m_spellCheckQueue.empty();
749
for(int line = newDisplayRange.end().line(); line >= newDisplayRange.start().line(); --line) {
750
if(!oldDisplayRange.containsLine(line)) {
751
bool visible = false;
752
foreach(KTextEditor::View *it2, m_document->views()) {
753
KateView *view2 = static_cast<KateView*>(it2);
754
if(view != view2 && view2->visibleRange().containsLine(line)) {
760
queueLineSpellCheck(m_document, line);
764
if(emptyAtStart && !m_spellCheckQueue.isEmpty()) {
765
QTimer::singleShot(0, this, SLOT(performSpellCheck()));
770
void KateOnTheFlyChecker::queueSpellCheckVisibleRange(const KTextEditor::Range& range)
772
const QList<KTextEditor::View*>& viewList = m_document->views();
773
for(QList<KTextEditor::View*>::const_iterator i = viewList.begin(); i != viewList.end(); ++i) {
774
queueSpellCheckVisibleRange(static_cast<KateView*>(*i), range);
778
void KateOnTheFlyChecker::queueSpellCheckVisibleRange(KateView *view, const KTextEditor::Range& range)
780
Q_ASSERT(m_document == view->doc());
781
KTextEditor::Range visibleRange = view->visibleRange();
782
KTextEditor::Range intersection = visibleRange.intersect(range);
783
if(intersection.isEmpty()) {
787
// clear all the highlights that are currently present in the range that
788
// is supposed to be checked, necessary due to highlighting
789
const MovingRangeList highlightsList = installedMovingRanges(intersection);
790
deleteMovingRanges(highlightsList);
792
QList<QPair<KTextEditor::Range, QString> > spellCheckRanges
793
= KateGlobal::self()->spellCheckManager()->spellCheckRanges(m_document,
796
//we queue them up in reverse
797
QListIterator<QPair<KTextEditor::Range, QString> > i(spellCheckRanges);
799
while(i.hasPrevious()) {
800
QPair<KTextEditor::Range, QString> p = i.previous();
801
queueLineSpellCheck(p.first, p.second);
805
void KateOnTheFlyChecker::queueLineSpellCheck(KateDocument *kateDocument, int line)
807
const KTextEditor::Range range = KTextEditor::Range(line, 0, line, kateDocument->lineLength(line));
808
// clear all the highlights that are currently present in the range that
809
// is supposed to be checked, necessary due to highlighting
811
const MovingRangeList highlightsList = installedMovingRanges(range);
812
deleteMovingRanges(highlightsList);
814
QList<QPair<KTextEditor::Range, QString> > spellCheckRanges
815
= KateGlobal::self()->spellCheckManager()->spellCheckRanges(kateDocument,
818
//we queue them up in reverse
819
QListIterator<QPair<KTextEditor::Range, QString> > i(spellCheckRanges);
821
while(i.hasPrevious()) {
822
QPair<KTextEditor::Range, QString> p = i.previous();
823
queueLineSpellCheck(p.first, p.second);
827
void KateOnTheFlyChecker::queueLineSpellCheck(const KTextEditor::Range& range, const QString& dictionary)
829
ON_THE_FLY_DEBUG << m_document << range;
831
Q_ASSERT(range.onSingleLine());
833
if(range.isEmpty()) {
837
addToSpellCheckQueue(range, dictionary);
840
void KateOnTheFlyChecker::addToSpellCheckQueue(const KTextEditor::Range& range, const QString& dictionary)
842
addToSpellCheckQueue(m_document->newMovingRange(range), dictionary);
845
void KateOnTheFlyChecker::addToSpellCheckQueue(KTextEditor::MovingRange *range, const QString& dictionary)
847
ON_THE_FLY_DEBUG << m_document << *range << dictionary;
849
range->setFeedback(this);
851
// if the queue contains a subrange of 'range', we remove that one
852
for(QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin();
853
i != m_spellCheckQueue.end();) {
854
KTextEditor::MovingRange *spellCheckRange = (*i).first;
855
if(range->contains(*spellCheckRange)) {
856
deleteMovingRangeQuickly(spellCheckRange);
857
i = m_spellCheckQueue.erase(i);
863
// leave 'push_front' here as it is a LIFO queue, i.e. a stack
864
m_spellCheckQueue.push_front(SpellCheckItem(range, dictionary));
865
ON_THE_FLY_DEBUG << "added"
866
<< *range << dictionary
867
<< "to the queue, which has a length of" << m_spellCheckQueue.size();
870
void KateOnTheFlyChecker::viewRefreshTimeout()
873
updateInstalledMovingRanges(m_refreshView);
875
m_refreshView = NULL;
878
void KateOnTheFlyChecker::restartViewRefreshTimer(KateView *view)
880
if(m_refreshView && view != m_refreshView) { // a new view should be refreshed
881
updateInstalledMovingRanges(m_refreshView); // so refresh the old one first
883
m_refreshView = view;
884
m_viewRefreshTimer->start(100);
887
void KateOnTheFlyChecker::deleteMovingRangeQuickly(KTextEditor::MovingRange *range)
889
range->setFeedback(NULL);
890
foreach(KTextEditor::View *view, m_document->views()) {
891
static_cast<KateView*>(view)->spellingMenu()->rangeDeleted(range);
896
void KateOnTheFlyChecker::handleModifiedRanges()
898
foreach(const ModificationItem &item, m_modificationList) {
899
KTextEditor::MovingRange *movingRange = item.second;
900
KTextEditor::Range range = *movingRange;
901
deleteMovingRangeQuickly(movingRange);
902
if(item.first == TEXT_INSERTED) {
903
handleInsertedText(range);
906
handleRemovedText(range);
909
m_modificationList.clear();
912
bool KateOnTheFlyChecker::removeRangeFromModificationList(KTextEditor::MovingRange *range)
915
for(ModificationList::iterator i = m_modificationList.begin(); i != m_modificationList.end();) {
916
ModificationItem item = *i;
917
KTextEditor::MovingRange *movingRange = item.second;
918
if(movingRange == range) {
920
i = m_modificationList.erase(i);
929
void KateOnTheFlyChecker::clearModificationList()
931
foreach(const ModificationItem &item, m_modificationList) {
932
KTextEditor::MovingRange *movingRange = item.second;
933
deleteMovingRangeQuickly(movingRange);
935
m_modificationList.clear();
938
#include "ontheflycheck.moc"
940
// kate: space-indent on; indent-width 2; replace-tabs on;