diff --git a/src/spellcheck/ontheflycheck.cpp b/src/spellcheck/ontheflycheck.cpp index 05c871de..735c2667 100644 --- a/src/spellcheck/ontheflycheck.cpp +++ b/src/spellcheck/ontheflycheck.cpp @@ -1,908 +1,929 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2008-2010 by Michel Ludwig * Copyright (C) 2009 by Joseph Wenninger * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ /* If ever threads should be used again, thread communication and * synchronization ought to be done with blocking queued signal connections. */ #include "ontheflycheck.h" #include #include "kateconfig.h" #include "kateglobal.h" #include "katerenderer.h" #include "katebuffer.h" #include "kateview.h" #include "spellcheck.h" #include "spellingmenu.h" #include "katepartdebug.h" #define ON_THE_FLY_DEBUG qCDebug(LOG_KTE) namespace { inline const QPair& invalidSpellCheckQueueItem() { static const auto item = QPair(nullptr, QString()); return item; } } KateOnTheFlyChecker::KateOnTheFlyChecker(KTextEditor::DocumentPrivate *document) : QObject(document), m_document(document), m_backgroundChecker(nullptr), m_currentlyCheckedItem(invalidSpellCheckQueueItem()), m_refreshView(nullptr) { ON_THE_FLY_DEBUG << "created"; m_viewRefreshTimer = new QTimer(this); m_viewRefreshTimer->setSingleShot(true); connect(m_viewRefreshTimer, SIGNAL(timeout()), this, SLOT(viewRefreshTimeout())); connect(document, &KTextEditor::DocumentPrivate::textInserted, this, &KateOnTheFlyChecker::textInserted); connect(document, &KTextEditor::DocumentPrivate::textRemoved, this, &KateOnTheFlyChecker::textRemoved); connect(document, SIGNAL(viewCreated(KTextEditor::Document*,KTextEditor::View*)), this, SLOT(addView(KTextEditor::Document*,KTextEditor::View*))); connect(document, SIGNAL(highlightingModeChanged(KTextEditor::Document*)), this, SLOT(updateConfig())); connect(&document->buffer(), SIGNAL(respellCheckBlock(int,int)), this, SLOT(handleRespellCheckBlock(int,int))); // load the settings for the speller updateConfig(); foreach (KTextEditor::View *view, document->views()) { addView(document, view); } refreshSpellCheck(); } KateOnTheFlyChecker::~KateOnTheFlyChecker() { freeDocument(); } QPair KateOnTheFlyChecker::getMisspelledItem(const KTextEditor::Cursor &cursor) const { foreach (const MisspelledItem &item, m_misspelledList) { KTextEditor::MovingRange *movingRange = item.first; if (movingRange->contains(cursor)) { return QPair(*movingRange, item.second); } } return QPair(KTextEditor::Range::invalid(), QString()); } QString KateOnTheFlyChecker::dictionaryForMisspelledRange(const KTextEditor::Range &range) const { foreach (const MisspelledItem &item, m_misspelledList) { KTextEditor::MovingRange *movingRange = item.first; if (*movingRange == range) { return item.second; } } return QString(); } void KateOnTheFlyChecker::clearMisspellingForWord(const QString &word) { MisspelledList misspelledList = m_misspelledList; // make a copy foreach (const MisspelledItem &item, misspelledList) { KTextEditor::MovingRange *movingRange = item.first; if (m_document->text(*movingRange) == word) { deleteMovingRange(movingRange); } } } void KateOnTheFlyChecker::handleRespellCheckBlock(int start, int end) { ON_THE_FLY_DEBUG << start << end; KTextEditor::Range range(start, 0, end, m_document->lineLength(end)); bool listEmpty = m_modificationList.isEmpty(); KTextEditor::MovingRange *movingRange = m_document->newMovingRange(range); movingRange->setFeedback(this); // we don't handle this directly as the highlighting information might not be up-to-date yet m_modificationList.push_back(ModificationItem(TEXT_INSERTED, movingRange)); ON_THE_FLY_DEBUG << "added" << *movingRange; if (listEmpty) { QTimer::singleShot(0, this, SLOT(handleModifiedRanges())); } } void KateOnTheFlyChecker::textInserted(KTextEditor::Document *document, const KTextEditor::Range &range) { Q_ASSERT(document == m_document); Q_UNUSED(document); if (!range.isValid()) { return; } bool listEmptyAtStart = m_modificationList.isEmpty(); // don't consider a range that is not within the document range const KTextEditor::Range documentIntersection = m_document->documentRange().intersect(range); if (!documentIntersection.isValid()) { return; } // for performance reasons we only want to schedule spellchecks for ranges that are visible foreach (KTextEditor::View *i, m_document->views()) { KTextEditor::ViewPrivate *view = static_cast(i); KTextEditor::Range visibleIntersection = documentIntersection.intersect(view->visibleRange()); if (visibleIntersection.isValid()) { // allow empty intersections // we don't handle this directly as the highlighting information might not be up-to-date yet KTextEditor::MovingRange *movingRange = m_document->newMovingRange(visibleIntersection); movingRange->setFeedback(this); m_modificationList.push_back(ModificationItem(TEXT_INSERTED, movingRange)); ON_THE_FLY_DEBUG << "added" << *movingRange; } } if (listEmptyAtStart && !m_modificationList.isEmpty()) { QTimer::singleShot(0, this, SLOT(handleModifiedRanges())); } } void KateOnTheFlyChecker::handleInsertedText(const KTextEditor::Range &range) { KTextEditor::Range consideredRange = range; ON_THE_FLY_DEBUG << m_document << range; bool spellCheckInProgress = m_currentlyCheckedItem != invalidSpellCheckQueueItem(); if (spellCheckInProgress) { KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first; if (spellCheckRange->contains(consideredRange)) { consideredRange = *spellCheckRange; stopCurrentSpellCheck(); deleteMovingRangeQuickly(spellCheckRange); } else if (consideredRange.contains(*spellCheckRange)) { stopCurrentSpellCheck(); deleteMovingRangeQuickly(spellCheckRange); } else if (consideredRange.overlaps(*spellCheckRange)) { consideredRange.expandToRange(*spellCheckRange); stopCurrentSpellCheck(); deleteMovingRangeQuickly(spellCheckRange); } else { spellCheckInProgress = false; } } for (QList::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { KTextEditor::MovingRange *spellCheckRange = (*i).first; if (spellCheckRange->contains(consideredRange)) { consideredRange = *spellCheckRange; ON_THE_FLY_DEBUG << "erasing range " << *i; i = m_spellCheckQueue.erase(i); deleteMovingRangeQuickly(spellCheckRange); } else if (consideredRange.contains(*spellCheckRange)) { ON_THE_FLY_DEBUG << "erasing range " << *i; i = m_spellCheckQueue.erase(i); deleteMovingRangeQuickly(spellCheckRange); } else if (consideredRange.overlaps(*spellCheckRange)) { consideredRange.expandToRange(*spellCheckRange); ON_THE_FLY_DEBUG << "erasing range " << *i; i = m_spellCheckQueue.erase(i); deleteMovingRangeQuickly(spellCheckRange); } else { ++i; } } KTextEditor::Range spellCheckRange = findWordBoundaries(consideredRange.start(), consideredRange.end()); const bool emptyAtStart = m_spellCheckQueue.isEmpty(); queueSpellCheckVisibleRange(spellCheckRange); if (spellCheckInProgress || (emptyAtStart && !m_spellCheckQueue.isEmpty())) { QTimer::singleShot(0, this, SLOT(performSpellCheck())); } } void KateOnTheFlyChecker::textRemoved(KTextEditor::Document *document, const KTextEditor::Range &range) { Q_ASSERT(document == m_document); Q_UNUSED(document); if (!range.isValid()) { return; } bool listEmptyAtStart = m_modificationList.isEmpty(); // don't consider a range that is behind the end of the document const KTextEditor::Range documentIntersection = m_document->documentRange().intersect(range); if (!documentIntersection.isValid()) { // the intersection might however be empty if the last return; // word has been removed, for example } // for performance reasons we only want to schedule spellchecks for ranges that are visible foreach (KTextEditor::View *i, m_document->views()) { KTextEditor::ViewPrivate *view = static_cast(i); KTextEditor::Range visibleIntersection = documentIntersection.intersect(view->visibleRange()); if (visibleIntersection.isValid()) { // see above // we don't handle this directly as the highlighting information might not be up-to-date yet KTextEditor::MovingRange *movingRange = m_document->newMovingRange(visibleIntersection); movingRange->setFeedback(this); m_modificationList.push_back(ModificationItem(TEXT_REMOVED, movingRange)); ON_THE_FLY_DEBUG << "added" << *movingRange << view->visibleRange(); } } if (listEmptyAtStart && !m_modificationList.isEmpty()) { QTimer::singleShot(0, this, SLOT(handleModifiedRanges())); } } inline bool rangesAdjacent(const KTextEditor::Range &r1, const KTextEditor::Range &r2) { return (r1.end() == r2.start()) || (r2.end() == r1.start()); } void KateOnTheFlyChecker::handleRemovedText(const KTextEditor::Range &range) { ON_THE_FLY_DEBUG << range; QList rangesToReCheck; for (QList::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { KTextEditor::MovingRange *spellCheckRange = (*i).first; if (rangesAdjacent(*spellCheckRange, range) || spellCheckRange->contains(range)) { ON_THE_FLY_DEBUG << "erasing range " << *i; if (!spellCheckRange->isEmpty()) { rangesToReCheck.push_back(*spellCheckRange); } deleteMovingRangeQuickly(spellCheckRange); i = m_spellCheckQueue.erase(i); } else { ++i; } } bool spellCheckInProgress = m_currentlyCheckedItem != invalidSpellCheckQueueItem(); const bool emptyAtStart = m_spellCheckQueue.isEmpty(); if (spellCheckInProgress) { KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first; ON_THE_FLY_DEBUG << *spellCheckRange; if (m_document->documentRange().contains(*spellCheckRange) && (rangesAdjacent(*spellCheckRange, range) || spellCheckRange->contains(range)) && !spellCheckRange->isEmpty()) { rangesToReCheck.push_back(*spellCheckRange); ON_THE_FLY_DEBUG << "added the range " << *spellCheckRange; stopCurrentSpellCheck(); deleteMovingRangeQuickly(spellCheckRange); } else if (spellCheckRange->isEmpty()) { stopCurrentSpellCheck(); deleteMovingRangeQuickly(spellCheckRange); } else { spellCheckInProgress = false; } } for (QList::iterator i = rangesToReCheck.begin(); i != rangesToReCheck.end(); ++i) { queueSpellCheckVisibleRange(*i); } KTextEditor::Range spellCheckRange = findWordBoundaries(range.start(), range.start()); KTextEditor::Cursor spellCheckEnd = spellCheckRange.end(); queueSpellCheckVisibleRange(spellCheckRange); if (range.numberOfLines() > 0) { //FIXME: there is no currently no way of doing this better as we only get notifications for removals of // of single lines, i.e. we don't know here how many lines have been removed in total KTextEditor::Cursor nextLineStart(spellCheckEnd.line() + 1, 0); const KTextEditor::Cursor documentEnd = m_document->documentEnd(); if (nextLineStart < documentEnd) { KTextEditor::Range rangeBelow = KTextEditor::Range(nextLineStart, documentEnd); const QList &viewList = m_document->views(); for (QList::const_iterator i = viewList.begin(); i != viewList.end(); ++i) { KTextEditor::ViewPrivate *view = static_cast(*i); const KTextEditor::Range visibleRange = view->visibleRange(); KTextEditor::Range intersection = visibleRange.intersect(rangeBelow); if (intersection.isValid()) { queueSpellCheckVisibleRange(view, intersection); } } } } ON_THE_FLY_DEBUG << "finished"; if (spellCheckInProgress || (emptyAtStart && !m_spellCheckQueue.isEmpty())) { QTimer::singleShot(0, this, SLOT(performSpellCheck())); } } void KateOnTheFlyChecker::freeDocument() { ON_THE_FLY_DEBUG; // empty the spell check queue for (QList::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { ON_THE_FLY_DEBUG << "erasing range " << *i; KTextEditor::MovingRange *movingRange = (*i).first; deleteMovingRangeQuickly(movingRange); i = m_spellCheckQueue.erase(i); } if (m_currentlyCheckedItem != invalidSpellCheckQueueItem()) { KTextEditor::MovingRange *movingRange = m_currentlyCheckedItem.first; deleteMovingRangeQuickly(movingRange); } stopCurrentSpellCheck(); MisspelledList misspelledList = m_misspelledList; // make a copy! foreach (const MisspelledItem &i, misspelledList) { deleteMovingRange(i.first); } m_misspelledList.clear(); clearModificationList(); } void KateOnTheFlyChecker::performSpellCheck() { if (m_currentlyCheckedItem != invalidSpellCheckQueueItem()) { ON_THE_FLY_DEBUG << "exited as a check is currently in progress"; return; } if (m_spellCheckQueue.isEmpty()) { ON_THE_FLY_DEBUG << "exited as there is nothing to do"; return; } m_currentlyCheckedItem = m_spellCheckQueue.takeFirst(); KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first; const QString &language = m_currentlyCheckedItem.second; ON_THE_FLY_DEBUG << "for the range " << *spellCheckRange; // clear all the highlights that are currently present in the range that // is supposed to be checked const MovingRangeList highlightsList = installedMovingRanges(*spellCheckRange); // make a copy! deleteMovingRanges(highlightsList); m_currentDecToEncOffsetList.clear(); KTextEditor::DocumentPrivate::OffsetList encToDecOffsetList; QString text = m_document->decodeCharacters(*spellCheckRange, m_currentDecToEncOffsetList, encToDecOffsetList); ON_THE_FLY_DEBUG << "next spell checking" << text; if (text.isEmpty()) { // passing an empty string to Sonnet can lead to a bad allocation exception spellCheckDone(); // (bug 225867) return; } if (m_speller.language() != language) { m_speller.setLanguage(language); } if (!m_backgroundChecker) { m_backgroundChecker = new Sonnet::BackgroundChecker(m_speller, this); connect(m_backgroundChecker, SIGNAL(misspelling(QString,int)), this, SLOT(misspelling(QString,int))); connect(m_backgroundChecker, SIGNAL(done()), this, SLOT(spellCheckDone())); + + KateSpellCheckManager *m_spellCheckManager = KTextEditor::EditorPrivate::self()->spellCheckManager(); + connect(m_spellCheckManager, &KateSpellCheckManager::wordAddedToDictionary, + this, &KateOnTheFlyChecker::addToDictionary); + connect(m_spellCheckManager, &KateSpellCheckManager::wordIgnored, + this, &KateOnTheFlyChecker::addToSession); } m_backgroundChecker->setSpeller(m_speller); m_backgroundChecker->setText(text); // don't call 'start()' after this! } +void KateOnTheFlyChecker::addToDictionary(const QString &word) +{ + if (m_backgroundChecker) { + m_backgroundChecker->addWordToPersonal(word); + } +} + +void KateOnTheFlyChecker::addToSession(const QString &word) +{ + if (m_backgroundChecker) { + m_backgroundChecker->addWordToSession(word); + } +} + + void KateOnTheFlyChecker::removeRangeFromEverything(KTextEditor::MovingRange *movingRange) { Q_ASSERT(m_document == movingRange->document()); ON_THE_FLY_DEBUG << *movingRange << "(" << movingRange << ")"; if (removeRangeFromModificationList(movingRange)) { return; // range was part of the modification queue, so we don't have // to look further for it } if (removeRangeFromSpellCheckQueue(movingRange)) { return; // range was part of the spell check queue, so it cannot have been // a misspelled range } for (MisspelledList::iterator i = m_misspelledList.begin(); i != m_misspelledList.end();) { if ((*i).first == movingRange) { i = m_misspelledList.erase(i); } else { ++i; } } } bool KateOnTheFlyChecker::removeRangeFromCurrentSpellCheck(KTextEditor::MovingRange *range) { if (m_currentlyCheckedItem != invalidSpellCheckQueueItem() && m_currentlyCheckedItem.first == range) { stopCurrentSpellCheck(); return true; } return false; } void KateOnTheFlyChecker::stopCurrentSpellCheck() { m_currentDecToEncOffsetList.clear(); m_currentlyCheckedItem = invalidSpellCheckQueueItem(); if (m_backgroundChecker) { m_backgroundChecker->stop(); } } bool KateOnTheFlyChecker::removeRangeFromSpellCheckQueue(KTextEditor::MovingRange *range) { if (removeRangeFromCurrentSpellCheck(range)) { if (!m_spellCheckQueue.isEmpty()) { QTimer::singleShot(0, this, SLOT(performSpellCheck())); } return true; } bool found = false; for (QList::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { if ((*i).first == range) { i = m_spellCheckQueue.erase(i); found = true; } else { ++i; } } return found; } void KateOnTheFlyChecker::rangeEmpty(KTextEditor::MovingRange *range) { ON_THE_FLY_DEBUG << range->start() << range->end() << "(" << range << ")"; deleteMovingRange(range); } void KateOnTheFlyChecker::rangeInvalid(KTextEditor::MovingRange *range) { ON_THE_FLY_DEBUG << range->start() << range->end() << "(" << range << ")"; deleteMovingRange(range); } void KateOnTheFlyChecker::mouseEnteredRange(KTextEditor::MovingRange *range, KTextEditor::View *view) { KTextEditor::ViewPrivate *kateView = static_cast(view); kateView->spellingMenu()->mouseEnteredMisspelledRange(range); } void KateOnTheFlyChecker::mouseExitedRange(KTextEditor::MovingRange *range, KTextEditor::View *view) { KTextEditor::ViewPrivate *kateView = static_cast(view); kateView->spellingMenu()->mouseExitedMisspelledRange(range); } /** * It is not enough to use 'caret/Entered/ExitedRange' only as the cursor doesn't move when some * text has been selected. **/ void KateOnTheFlyChecker::caretEnteredRange(KTextEditor::MovingRange *range, KTextEditor::View *view) { KTextEditor::ViewPrivate *kateView = static_cast(view); kateView->spellingMenu()->caretEnteredMisspelledRange(range); } void KateOnTheFlyChecker::caretExitedRange(KTextEditor::MovingRange *range, KTextEditor::View *view) { KTextEditor::ViewPrivate *kateView = static_cast(view); kateView->spellingMenu()->caretExitedMisspelledRange(range); } void KateOnTheFlyChecker::deleteMovingRange(KTextEditor::MovingRange *range) { ON_THE_FLY_DEBUG << range; // remove it from all our structures removeRangeFromEverything(range); range->setFeedback(nullptr); foreach (KTextEditor::View *view, m_document->views()) { static_cast(view)->spellingMenu()->rangeDeleted(range); } delete(range); } void KateOnTheFlyChecker::deleteMovingRanges(const QList &list) { foreach (KTextEditor::MovingRange *r, list) { deleteMovingRange(r); } } KTextEditor::Range KateOnTheFlyChecker::findWordBoundaries(const KTextEditor::Cursor &begin, const KTextEditor::Cursor &end) { // FIXME: QTextBoundaryFinder should be ideally used for this, but it is currently // still broken in Qt const QRegExp boundaryRegExp(QLatin1String("\\b")); const QRegExp boundaryQuoteRegExp(QLatin1String("\\b\\w+'\\w*$")); // handle spell checking of QLatin1String("isn't"), QLatin1String("doesn't"), etc. const QRegExp extendedBoundaryRegExp(QLatin1String("(\\W|$)")); const QRegExp extendedBoundaryQuoteRegExp(QLatin1String("^\\w*'\\w+\\b")); // see above KTextEditor::DocumentPrivate::OffsetList decToEncOffsetList, encToDecOffsetList; const int startLine = begin.line(); const int startColumn = begin.column(); KTextEditor::Cursor boundaryStart, boundaryEnd; // first we take care of the start position const KTextEditor::Range startLineRange(startLine, 0, startLine, m_document->lineLength(startLine)); QString decodedLineText = m_document->decodeCharacters(startLineRange, decToEncOffsetList, encToDecOffsetList); int translatedColumn = m_document->computePositionWrtOffsets(encToDecOffsetList, startColumn); QString text = decodedLineText.mid(0, translatedColumn); boundaryStart.setLine(startLine); int match = text.lastIndexOf(boundaryQuoteRegExp); if (match < 0) { match = text.lastIndexOf(boundaryRegExp); } boundaryStart.setColumn(m_document->computePositionWrtOffsets(decToEncOffsetList, qMax(0, match))); // and now the end position const int endLine = end.line(); const int endColumn = end.column(); if (endLine != startLine) { decToEncOffsetList.clear(); encToDecOffsetList.clear(); const KTextEditor::Range endLineRange(endLine, 0, endLine, m_document->lineLength(endLine)); decodedLineText = m_document->decodeCharacters(endLineRange, decToEncOffsetList, encToDecOffsetList); } translatedColumn = m_document->computePositionWrtOffsets(encToDecOffsetList, endColumn); text = decodedLineText.mid(translatedColumn); boundaryEnd.setLine(endLine); match = extendedBoundaryQuoteRegExp.indexIn(text); if (match == 0) { match = extendedBoundaryQuoteRegExp.matchedLength(); } else { match = extendedBoundaryRegExp.indexIn(text); } boundaryEnd.setColumn(m_document->computePositionWrtOffsets(decToEncOffsetList, translatedColumn + qMax(0, match))); return KTextEditor::Range(boundaryStart, boundaryEnd); } void KateOnTheFlyChecker::misspelling(const QString &word, int start) { if (m_currentlyCheckedItem == invalidSpellCheckQueueItem()) { ON_THE_FLY_DEBUG << "exited as no spell check is taking place"; return; } int translatedStart = m_document->computePositionWrtOffsets(m_currentDecToEncOffsetList, start); // ON_THE_FLY_DEBUG << "misspelled " << word // << " at line " // << *m_currentlyCheckedItem.first // << " column " << start; KTextEditor::MovingRange *spellCheckRange = m_currentlyCheckedItem.first; int line = spellCheckRange->start().line(); int rangeStart = spellCheckRange->start().column(); int translatedEnd = m_document->computePositionWrtOffsets(m_currentDecToEncOffsetList, start + word.length()); KTextEditor::MovingRange *movingRange = m_document->newMovingRange(KTextEditor::Range(line, rangeStart + translatedStart, line, rangeStart + translatedEnd)); movingRange->setFeedback(this); KTextEditor::Attribute *attribute = new KTextEditor::Attribute(); attribute->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); attribute->setUnderlineColor(KateRendererConfig::global()->spellingMistakeLineColor()); // don't print this range movingRange->setAttributeOnlyForViews(true); movingRange->setAttribute(KTextEditor::Attribute::Ptr(attribute)); m_misspelledList.push_back(MisspelledItem(movingRange, m_currentlyCheckedItem.second)); if (m_backgroundChecker) { m_backgroundChecker->continueChecking(); } } void KateOnTheFlyChecker::spellCheckDone() { ON_THE_FLY_DEBUG << "on-the-fly spell check done, queue length " << m_spellCheckQueue.size(); if (m_currentlyCheckedItem == invalidSpellCheckQueueItem()) { return; } KTextEditor::MovingRange *movingRange = m_currentlyCheckedItem.first; stopCurrentSpellCheck(); deleteMovingRangeQuickly(movingRange); if (!m_spellCheckQueue.empty()) { QTimer::singleShot(0, this, SLOT(performSpellCheck())); } } QList KateOnTheFlyChecker::installedMovingRanges(const KTextEditor::Range &range) { ON_THE_FLY_DEBUG << range; MovingRangeList toReturn; for (QList::iterator i = m_misspelledList.begin(); i != m_misspelledList.end(); ++i) { KTextEditor::MovingRange *movingRange = (*i).first; if (movingRange->overlaps(range)) { toReturn.push_back(movingRange); } } return toReturn; } void KateOnTheFlyChecker::updateConfig() { ON_THE_FLY_DEBUG; //m_speller.restore(); } void KateOnTheFlyChecker::refreshSpellCheck(const KTextEditor::Range &range) { if (range.isValid()) { textInserted(m_document, range); } else { freeDocument(); textInserted(m_document, m_document->documentRange()); } } void KateOnTheFlyChecker::addView(KTextEditor::Document *document, KTextEditor::View *view) { Q_ASSERT(document == m_document); Q_UNUSED(document); ON_THE_FLY_DEBUG; connect(view, SIGNAL(destroyed(QObject*)), this, SLOT(viewDestroyed(QObject*))); connect(view, SIGNAL(displayRangeChanged(KTextEditor::ViewPrivate*)), this, SLOT(restartViewRefreshTimer(KTextEditor::ViewPrivate*))); updateInstalledMovingRanges(static_cast(view)); } void KateOnTheFlyChecker::viewDestroyed(QObject *obj) { ON_THE_FLY_DEBUG; KTextEditor::View *view = static_cast(obj); m_displayRangeMap.remove(view); } void KateOnTheFlyChecker::removeView(KTextEditor::View *view) { ON_THE_FLY_DEBUG; m_displayRangeMap.remove(view); } void KateOnTheFlyChecker::updateInstalledMovingRanges(KTextEditor::ViewPrivate *view) { Q_ASSERT(m_document == view->document()); ON_THE_FLY_DEBUG; KTextEditor::Range oldDisplayRange = m_displayRangeMap[view]; KTextEditor::Range newDisplayRange = view->visibleRange(); ON_THE_FLY_DEBUG << "new range: " << newDisplayRange; ON_THE_FLY_DEBUG << "old range: " << oldDisplayRange; QList toDelete; foreach (const MisspelledItem &item, m_misspelledList) { KTextEditor::MovingRange *movingRange = item.first; if (!movingRange->overlaps(newDisplayRange)) { bool stillVisible = false; foreach (KTextEditor::View *it2, m_document->views()) { KTextEditor::ViewPrivate *view2 = static_cast(it2); if (view != view2 && movingRange->overlaps(view2->visibleRange())) { stillVisible = true; break; } } if (!stillVisible) { toDelete.push_back(movingRange); } } } deleteMovingRanges(toDelete); m_displayRangeMap[view] = newDisplayRange; if (oldDisplayRange.isValid()) { bool emptyAtStart = m_spellCheckQueue.empty(); for (int line = newDisplayRange.end().line(); line >= newDisplayRange.start().line(); --line) { if (!oldDisplayRange.containsLine(line)) { bool visible = false; foreach (KTextEditor::View *it2, m_document->views()) { KTextEditor::ViewPrivate *view2 = static_cast(it2); if (view != view2 && view2->visibleRange().containsLine(line)) { visible = true; break; } } if (!visible) { queueLineSpellCheck(m_document, line); } } } if (emptyAtStart && !m_spellCheckQueue.isEmpty()) { QTimer::singleShot(0, this, SLOT(performSpellCheck())); } } } void KateOnTheFlyChecker::queueSpellCheckVisibleRange(const KTextEditor::Range &range) { const QList &viewList = m_document->views(); for (QList::const_iterator i = viewList.begin(); i != viewList.end(); ++i) { queueSpellCheckVisibleRange(static_cast(*i), range); } } void KateOnTheFlyChecker::queueSpellCheckVisibleRange(KTextEditor::ViewPrivate *view, const KTextEditor::Range &range) { Q_ASSERT(m_document == view->doc()); KTextEditor::Range visibleRange = view->visibleRange(); KTextEditor::Range intersection = visibleRange.intersect(range); if (intersection.isEmpty()) { return; } // clear all the highlights that are currently present in the range that // is supposed to be checked, necessary due to highlighting const MovingRangeList highlightsList = installedMovingRanges(intersection); deleteMovingRanges(highlightsList); QList > spellCheckRanges = KTextEditor::EditorPrivate::self()->spellCheckManager()->spellCheckRanges(m_document, intersection, true); //we queue them up in reverse QListIterator > i(spellCheckRanges); i.toBack(); while (i.hasPrevious()) { QPair p = i.previous(); queueLineSpellCheck(p.first, p.second); } } void KateOnTheFlyChecker::queueLineSpellCheck(KTextEditor::DocumentPrivate *kateDocument, int line) { const KTextEditor::Range range = KTextEditor::Range(line, 0, line, kateDocument->lineLength(line)); // clear all the highlights that are currently present in the range that // is supposed to be checked, necessary due to highlighting const MovingRangeList highlightsList = installedMovingRanges(range); deleteMovingRanges(highlightsList); QList > spellCheckRanges = KTextEditor::EditorPrivate::self()->spellCheckManager()->spellCheckRanges(kateDocument, range, true); //we queue them up in reverse QListIterator > i(spellCheckRanges); i.toBack(); while (i.hasPrevious()) { QPair p = i.previous(); queueLineSpellCheck(p.first, p.second); } } void KateOnTheFlyChecker::queueLineSpellCheck(const KTextEditor::Range &range, const QString &dictionary) { ON_THE_FLY_DEBUG << m_document << range; Q_ASSERT(range.onSingleLine()); if (range.isEmpty()) { return; } addToSpellCheckQueue(range, dictionary); } void KateOnTheFlyChecker::addToSpellCheckQueue(const KTextEditor::Range &range, const QString &dictionary) { addToSpellCheckQueue(m_document->newMovingRange(range), dictionary); } void KateOnTheFlyChecker::addToSpellCheckQueue(KTextEditor::MovingRange *range, const QString &dictionary) { ON_THE_FLY_DEBUG << m_document << *range << dictionary; range->setFeedback(this); // if the queue contains a subrange of 'range', we remove that one for (QList::iterator i = m_spellCheckQueue.begin(); i != m_spellCheckQueue.end();) { KTextEditor::MovingRange *spellCheckRange = (*i).first; if (range->contains(*spellCheckRange)) { deleteMovingRangeQuickly(spellCheckRange); i = m_spellCheckQueue.erase(i); } else { ++i; } } // leave 'push_front' here as it is a LIFO queue, i.e. a stack m_spellCheckQueue.push_front(SpellCheckItem(range, dictionary)); ON_THE_FLY_DEBUG << "added" << *range << dictionary << "to the queue, which has a length of" << m_spellCheckQueue.size(); } void KateOnTheFlyChecker::viewRefreshTimeout() { if (m_refreshView) { updateInstalledMovingRanges(m_refreshView); } m_refreshView = nullptr; } void KateOnTheFlyChecker::restartViewRefreshTimer(KTextEditor::ViewPrivate *view) { if (m_refreshView && view != m_refreshView) { // a new view should be refreshed updateInstalledMovingRanges(m_refreshView); // so refresh the old one first } m_refreshView = view; m_viewRefreshTimer->start(100); } void KateOnTheFlyChecker::deleteMovingRangeQuickly(KTextEditor::MovingRange *range) { range->setFeedback(nullptr); foreach (KTextEditor::View *view, m_document->views()) { static_cast(view)->spellingMenu()->rangeDeleted(range); } delete(range); } void KateOnTheFlyChecker::handleModifiedRanges() { foreach (const ModificationItem &item, m_modificationList) { KTextEditor::MovingRange *movingRange = item.second; KTextEditor::Range range = *movingRange; deleteMovingRangeQuickly(movingRange); if (item.first == TEXT_INSERTED) { handleInsertedText(range); } else { handleRemovedText(range); } } m_modificationList.clear(); } bool KateOnTheFlyChecker::removeRangeFromModificationList(KTextEditor::MovingRange *range) { bool found = false; for (ModificationList::iterator i = m_modificationList.begin(); i != m_modificationList.end();) { ModificationItem item = *i; KTextEditor::MovingRange *movingRange = item.second; if (movingRange == range) { found = true; i = m_modificationList.erase(i); } else { ++i; } } return found; } void KateOnTheFlyChecker::clearModificationList() { foreach (const ModificationItem &item, m_modificationList) { KTextEditor::MovingRange *movingRange = item.second; deleteMovingRangeQuickly(movingRange); } m_modificationList.clear(); } diff --git a/src/spellcheck/ontheflycheck.h b/src/spellcheck/ontheflycheck.h index 2fd2d384..c5433ac7 100644 --- a/src/spellcheck/ontheflycheck.h +++ b/src/spellcheck/ontheflycheck.h @@ -1,141 +1,143 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2008-2010 by Michel Ludwig * Copyright (C) 2009 by Joseph Wenninger * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef ONTHEFLYCHECK_H #define ONTHEFLYCHECK_H #include #include #include #include #include #include #include #include "katedocument.h" namespace Sonnet { class BackgroundChecker; } class KateOnTheFlyChecker : public QObject, private KTextEditor::MovingRangeFeedback { Q_OBJECT enum ModificationType {TEXT_INSERTED = 0, TEXT_REMOVED}; typedef QPair SpellCheckItem; typedef QList MovingRangeList; typedef QPair MisspelledItem; typedef QList MisspelledList; typedef QPair ModificationItem; typedef QList ModificationList; public: explicit KateOnTheFlyChecker(KTextEditor::DocumentPrivate *document); ~KateOnTheFlyChecker() override; QPair getMisspelledItem(const KTextEditor::Cursor &cursor) const; QString dictionaryForMisspelledRange(const KTextEditor::Range &range) const; void clearMisspellingForWord(const QString &word); public Q_SLOTS: void textInserted(KTextEditor::Document *document, const KTextEditor::Range &range); void textRemoved(KTextEditor::Document *document, const KTextEditor::Range &range); void updateConfig(); void refreshSpellCheck(const KTextEditor::Range &range = KTextEditor::Range::invalid()); void updateInstalledMovingRanges(KTextEditor::ViewPrivate *view); protected: KTextEditor::DocumentPrivate *const m_document; Sonnet::Speller m_speller; QList m_spellCheckQueue; Sonnet::BackgroundChecker *m_backgroundChecker; SpellCheckItem m_currentlyCheckedItem; MisspelledList m_misspelledList; ModificationList m_modificationList; KTextEditor::DocumentPrivate::OffsetList m_currentDecToEncOffsetList; QMap m_displayRangeMap; void freeDocument(); MovingRangeList installedMovingRanges(const KTextEditor::Range &range); void queueLineSpellCheck(KTextEditor::DocumentPrivate *document, int line); /** * 'range' must be on a single line **/ void queueLineSpellCheck(const KTextEditor::Range &range, const QString &dictionary); void queueSpellCheckVisibleRange(const KTextEditor::Range &range); void queueSpellCheckVisibleRange(KTextEditor::ViewPrivate *view, const KTextEditor::Range &range); void addToSpellCheckQueue(const KTextEditor::Range &range, const QString &dictionary); void addToSpellCheckQueue(KTextEditor::MovingRange *range, const QString &dictionary); QTimer *m_viewRefreshTimer; QPointer m_refreshView; virtual void removeRangeFromEverything(KTextEditor::MovingRange *range); bool removeRangeFromCurrentSpellCheck(KTextEditor::MovingRange *range); bool removeRangeFromSpellCheckQueue(KTextEditor::MovingRange *range); void rangeEmpty(KTextEditor::MovingRange *range) override; void rangeInvalid(KTextEditor::MovingRange *range) override; void mouseEnteredRange(KTextEditor::MovingRange *range, KTextEditor::View *view) override; void mouseExitedRange(KTextEditor::MovingRange *range, KTextEditor::View *view) override; void caretEnteredRange(KTextEditor::MovingRange *range, KTextEditor::View *view) override; void caretExitedRange(KTextEditor::MovingRange *range, KTextEditor::View *view) override; KTextEditor::Range findWordBoundaries(const KTextEditor::Cursor &begin, const KTextEditor::Cursor &end); void deleteMovingRange(KTextEditor::MovingRange *range); void deleteMovingRanges(const QList &list); void deleteMovingRangeQuickly(KTextEditor::MovingRange *range); void stopCurrentSpellCheck(); protected Q_SLOTS: void performSpellCheck(); + void addToDictionary(const QString &word); + void addToSession(const QString &word); void misspelling(const QString &word, int start); void spellCheckDone(); void viewDestroyed(QObject *obj); void addView(KTextEditor::Document *document, KTextEditor::View *view); void removeView(KTextEditor::View *view); void restartViewRefreshTimer(KTextEditor::ViewPrivate *view); void viewRefreshTimeout(); void handleModifiedRanges(); void handleInsertedText(const KTextEditor::Range &range); void handleRemovedText(const KTextEditor::Range &range); void handleRespellCheckBlock(int start, int end); bool removeRangeFromModificationList(KTextEditor::MovingRange *range); void clearModificationList(); }; #endif diff --git a/src/spellcheck/spellcheck.cpp b/src/spellcheck/spellcheck.cpp index 19e8e169..87a380a7 100644 --- a/src/spellcheck/spellcheck.cpp +++ b/src/spellcheck/spellcheck.cpp @@ -1,287 +1,289 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2009 by Michel Ludwig * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ /* If ever threads should be used again, thread communication and * synchronization ought to be done with blocking queued signal connections. */ #include "spellcheck.h" #include #include #include #include #include #include #include "katedocument.h" #include "katehighlight.h" KateSpellCheckManager::KateSpellCheckManager(QObject *parent) : QObject(parent) { } KateSpellCheckManager::~KateSpellCheckManager() { } QStringList KateSpellCheckManager::suggestions(const QString &word, const QString &dictionary) { Sonnet::Speller speller; speller.setLanguage(dictionary); return speller.suggest(word); } void KateSpellCheckManager::ignoreWord(const QString &word, const QString &dictionary) { Sonnet::Speller speller; speller.setLanguage(dictionary); speller.addToSession(word); + emit wordIgnored(word); } void KateSpellCheckManager::addToDictionary(const QString &word, const QString &dictionary) { Sonnet::Speller speller; speller.setLanguage(dictionary); speller.addToPersonal(word); + emit wordAddedToDictionary(word); } QList KateSpellCheckManager::rangeDifference(const KTextEditor::Range &r1, const KTextEditor::Range &r2) { Q_ASSERT(r1.contains(r2)); QList toReturn; KTextEditor::Range before(r1.start(), r2.start()); KTextEditor::Range after(r2.end(), r1.end()); if (!before.isEmpty()) { toReturn.push_back(before); } if (!after.isEmpty()) { toReturn.push_back(after); } return toReturn; } namespace { bool lessThanRangeDictionaryPair(const QPair &s1, const QPair &s2) { return s1.first.end() <= s2.first.start(); } } QList > KateSpellCheckManager::spellCheckLanguageRanges(KTextEditor::DocumentPrivate *doc, const KTextEditor::Range &range) { QString defaultDict = doc->defaultDictionary(); QList toReturn; QList > dictionaryRanges = doc->dictionaryRanges(); if (dictionaryRanges.isEmpty()) { toReturn.push_back(RangeDictionaryPair(range, defaultDict)); return toReturn; } QList splitQueue; splitQueue.push_back(range); while (!splitQueue.isEmpty()) { bool handled = false; KTextEditor::Range consideredRange = splitQueue.takeFirst(); for (QList >::iterator i = dictionaryRanges.begin(); i != dictionaryRanges.end(); ++i) { KTextEditor::Range languageRange = *((*i).first); KTextEditor::Range intersection = languageRange.intersect(consideredRange); if (intersection.isEmpty()) { continue; } toReturn.push_back(RangeDictionaryPair(intersection, (*i).second)); splitQueue += rangeDifference(consideredRange, intersection); handled = true; break; } if (!handled) { // 'consideredRange' did not intersect with any dictionary range, so we add it with the default dictionary toReturn.push_back(RangeDictionaryPair(consideredRange, defaultDict)); } } // finally, we still have to sort the list qStableSort(toReturn.begin(), toReturn.end(), lessThanRangeDictionaryPair); return toReturn; } QList > KateSpellCheckManager::spellCheckWrtHighlightingRanges(KTextEditor::DocumentPrivate *document, const KTextEditor::Range &range, const QString &dictionary, bool singleLine, bool returnSingleRange) { QList > toReturn; if (range.isEmpty()) { return toReturn; } KateHighlighting *highlighting = document->highlight(); QList rangesToSplit; if (!singleLine || range.onSingleLine()) { rangesToSplit.push_back(range); } else { const int startLine = range.start().line(); const int startColumn = range.start().column(); const int endLine = range.end().line(); const int endColumn = range.end().column(); for (int line = startLine; line <= endLine; ++line) { const int start = (line == startLine) ? startColumn : 0; const int end = (line == endLine) ? endColumn : document->lineLength(line); KTextEditor::Range toAdd(line, start, line, end); if (!toAdd.isEmpty()) { rangesToSplit.push_back(toAdd); } } } for (QList::iterator i = rangesToSplit.begin(); i != rangesToSplit.end(); ++i) { KTextEditor::Range rangeToSplit = *i; KTextEditor::Cursor begin = KTextEditor::Cursor::invalid(); const int startLine = rangeToSplit.start().line(); const int startColumn = rangeToSplit.start().column(); const int endLine = rangeToSplit.end().line(); const int endColumn = rangeToSplit.end().column(); bool inSpellCheckArea = false; for (int line = startLine; line <= endLine; ++line) { Kate::TextLine kateTextLine = document->kateTextLine(line); if (!kateTextLine) { continue; // bug #303496 } const int start = (line == startLine) ? startColumn : 0; const int end = (line == endLine) ? endColumn : kateTextLine->length(); for (int i = start; i < end;) { // WARNING: 'i' has to be incremented manually! int attr = kateTextLine->attribute(i); const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr); QString prefixFound = prefixStore.findPrefix(kateTextLine, i); if (!document->highlight()->attributeRequiresSpellchecking(static_cast(attr)) && prefixFound.isEmpty()) { if (i == start) { ++i; continue; } else if (inSpellCheckArea) { KTextEditor::Range spellCheckRange(begin, KTextEditor::Cursor(line, i)); // work around Qt bug 6498 trimRange(document, spellCheckRange); if (!spellCheckRange.isEmpty()) { toReturn.push_back(RangeDictionaryPair(spellCheckRange, dictionary)); if (returnSingleRange) { return toReturn; } } begin = KTextEditor::Cursor::invalid(); inSpellCheckArea = false; } } else if (!inSpellCheckArea) { begin = KTextEditor::Cursor(line, i); inSpellCheckArea = true; } if (!prefixFound.isEmpty()) { i += prefixFound.length(); } else { ++i; } } } if (inSpellCheckArea) { KTextEditor::Range spellCheckRange(begin, rangeToSplit.end()); // work around Qt bug 6498 trimRange(document, spellCheckRange); if (!spellCheckRange.isEmpty()) { toReturn.push_back(RangeDictionaryPair(spellCheckRange, dictionary)); if (returnSingleRange) { return toReturn; } } } } return toReturn; } QList > KateSpellCheckManager::spellCheckRanges(KTextEditor::DocumentPrivate *doc, const KTextEditor::Range &range, bool singleLine) { QList toReturn; QList languageRangeList = spellCheckLanguageRanges(doc, range); for (QList::iterator i = languageRangeList.begin(); i != languageRangeList.end(); ++i) { const RangeDictionaryPair &p = *i; toReturn += spellCheckWrtHighlightingRanges(doc, p.first, p.second, singleLine); } return toReturn; } void KateSpellCheckManager::replaceCharactersEncodedIfNecessary(const QString &newWord, KTextEditor::DocumentPrivate *doc, const KTextEditor::Range &replacementRange) { const int attr = doc->kateTextLine(replacementRange.start().line())->attribute(replacementRange.start().column()); if (!doc->highlight()->getCharacterEncodings(attr).isEmpty() && doc->containsCharacterEncoding(replacementRange)) { doc->replaceText(replacementRange, newWord); doc->replaceCharactersByEncoding(KTextEditor::Range(replacementRange.start(), replacementRange.start() + KTextEditor::Cursor(0, newWord.length()))); } else { doc->replaceText(replacementRange, newWord); } } void KateSpellCheckManager::trimRange(KTextEditor::DocumentPrivate *doc, KTextEditor::Range &r) { if (r.isEmpty()) { return; } KTextEditor::Cursor cursor = r.start(); while (cursor < r.end()) { if (doc->lineLength(cursor.line()) > 0 && !doc->characterAt(cursor).isSpace() && doc->characterAt(cursor).category() != QChar::Other_Control) { break; } cursor.setColumn(cursor.column() + 1); if (cursor.column() >= doc->lineLength(cursor.line())) { cursor.setPosition(cursor.line() + 1, 0); } } r.setStart(cursor); if (r.isEmpty()) { return; } cursor = r.end(); KTextEditor::Cursor prevCursor = cursor; // the range cannot be empty now do { prevCursor = cursor; if (cursor.column() <= 0) { cursor.setPosition(cursor.line() - 1, doc->lineLength(cursor.line() - 1)); } else { cursor.setColumn(cursor.column() - 1); } if (cursor.column() < doc->lineLength(cursor.line()) && !doc->characterAt(cursor).isSpace() && doc->characterAt(cursor).category() != QChar::Other_Control) { break; } } while (cursor > r.start()); r.setEnd(prevCursor); } diff --git a/src/spellcheck/spellcheck.h b/src/spellcheck/spellcheck.h index ff0fc8fb..95b89383 100644 --- a/src/spellcheck/spellcheck.h +++ b/src/spellcheck/spellcheck.h @@ -1,72 +1,80 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2008-2009 by Michel Ludwig * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef SPELLCHECK_H #define SPELLCHECK_H #include #include #include #include #include #include #include namespace KTextEditor { class DocumentPrivate; } class KateSpellCheckManager : public QObject { Q_OBJECT typedef QPair RangeDictionaryPair; public: explicit KateSpellCheckManager(QObject *parent = nullptr); virtual ~KateSpellCheckManager(); QStringList suggestions(const QString &word, const QString &dictionary); void ignoreWord(const QString &word, const QString &dictionary); void addToDictionary(const QString &word, const QString &dictionary); /** * 'r2' is a subrange of 'r1', which is extracted from 'r1' and the remaining ranges are returned **/ static QList rangeDifference(const KTextEditor::Range &r1, const KTextEditor::Range &r2); +Q_SIGNALS: + /** + * These signals are used to propagate the dictionary changes to the + * BackgroundChecker instance in other components (e.g. onTheFlyChecker). + */ + void wordAddedToDictionary(const QString &word); + void wordIgnored(const QString &word); + public: QList > spellCheckLanguageRanges(KTextEditor::DocumentPrivate *doc, const KTextEditor::Range &range); QList > spellCheckWrtHighlightingRanges(KTextEditor::DocumentPrivate *doc, const KTextEditor::Range &range, const QString &dictionary = QString(), bool singleLine = false, bool returnSingleRange = false); QList > spellCheckRanges(KTextEditor::DocumentPrivate *doc, const KTextEditor::Range &range, bool singleLine = false); void replaceCharactersEncodedIfNecessary(const QString &newWord, KTextEditor::DocumentPrivate *doc, const KTextEditor::Range &replacementRange); private: void trimRange(KTextEditor::DocumentPrivate *doc, KTextEditor::Range &r); }; #endif