mirror of
https://bitbucket.org/smil3y/kde-workspace.git
synced 2025-02-24 10:52:51 +00:00
920 lines
33 KiB
C++
920 lines
33 KiB
C++
/* This file is part of the KDE libraries and the Kate part.
|
|
*
|
|
* Copyright (C) 2008-2010 by Michel Ludwig <michel.ludwig@kdemail.net>
|
|
* Copyright (C) 2009 by Joseph Wenninger <jowenn@kde.org>
|
|
*
|
|
* 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 <QTimer>
|
|
|
|
#include "kateconfig.h"
|
|
#include "kateglobal.h"
|
|
#include "katerenderer.h"
|
|
#include "katebuffer.h"
|
|
#include "kateview.h"
|
|
#include "spellcheck.h"
|
|
#include "spellingmenu.h"
|
|
|
|
#define ON_THE_FLY_DEBUG kDebug(debugArea())
|
|
|
|
KateOnTheFlyChecker::KateOnTheFlyChecker(KateDocument *document)
|
|
: QObject(document),
|
|
m_document(document),
|
|
m_speller(NULL),
|
|
m_currentlyCheckedItem(invalidSpellCheckQueueItem),
|
|
m_refreshView(NULL)
|
|
{
|
|
ON_THE_FLY_DEBUG << "created";
|
|
|
|
m_viewRefreshTimer = new QTimer(this);
|
|
m_viewRefreshTimer->setSingleShot(true);
|
|
connect(m_viewRefreshTimer, SIGNAL(timeout()), this, SLOT(viewRefreshTimeout()));
|
|
|
|
connect(document, SIGNAL(textInserted(KTextEditor::Document*,KTextEditor::Range)),
|
|
this, SLOT(textInserted(KTextEditor::Document*,KTextEditor::Range)));
|
|
connect(document, SIGNAL(textRemoved(KTextEditor::Document*,KTextEditor::Range)),
|
|
this, SLOT(textRemoved(KTextEditor::Document*,KTextEditor::Range)));
|
|
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)));
|
|
|
|
// creates speller and loads the settings for it
|
|
updateConfig();
|
|
|
|
foreach(KTextEditor::View* view, document->views()) {
|
|
addView(document, view);
|
|
}
|
|
refreshSpellCheck();
|
|
}
|
|
|
|
KateOnTheFlyChecker::~KateOnTheFlyChecker()
|
|
{
|
|
freeDocument();
|
|
}
|
|
|
|
// see kdebug.areas
|
|
int KateOnTheFlyChecker::debugArea()
|
|
{
|
|
return 13070;
|
|
}
|
|
|
|
QPair<KTextEditor::Range, QString> KateOnTheFlyChecker::getMisspelledItem(const KTextEditor::Cursor &cursor) const
|
|
{
|
|
foreach(const MisspelledItem &item, m_misspelledList) {
|
|
KTextEditor::MovingRange *movingRange = item.first;
|
|
if(movingRange->contains(cursor)) {
|
|
return QPair<KTextEditor::Range, QString>(movingRange->toRange(), item.second);
|
|
}
|
|
}
|
|
return QPair<KTextEditor::Range, QString>(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->toRange() == 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->toRange()) == word) {
|
|
deleteMovingRange(movingRange);
|
|
}
|
|
}
|
|
}
|
|
|
|
const KateOnTheFlyChecker::SpellCheckItem KateOnTheFlyChecker::invalidSpellCheckQueueItem =
|
|
SpellCheckItem(NULL, "");
|
|
|
|
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()) {
|
|
KateView *view = static_cast<KateView*>(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->toRange();
|
|
stopCurrentSpellCheck();
|
|
deleteMovingRangeQuickly(spellCheckRange);
|
|
}
|
|
else if(consideredRange.contains(spellCheckRange->toRange())) {
|
|
stopCurrentSpellCheck();
|
|
deleteMovingRangeQuickly(spellCheckRange);
|
|
}
|
|
else if(consideredRange.overlaps(spellCheckRange->toRange())) {
|
|
consideredRange.expandToRange(spellCheckRange->toRange());
|
|
stopCurrentSpellCheck();
|
|
deleteMovingRangeQuickly(spellCheckRange);
|
|
}
|
|
else {
|
|
spellCheckInProgress = false;
|
|
}
|
|
}
|
|
for(QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin();
|
|
i != m_spellCheckQueue.end();) {
|
|
KTextEditor::MovingRange *spellCheckRange = (*i).first;
|
|
if(spellCheckRange->contains(consideredRange)) {
|
|
consideredRange = spellCheckRange->toRange();
|
|
ON_THE_FLY_DEBUG << "erasing range " << *i;
|
|
i = m_spellCheckQueue.erase(i);
|
|
deleteMovingRangeQuickly(spellCheckRange);
|
|
}
|
|
else if(consideredRange.contains(spellCheckRange->toRange())) {
|
|
ON_THE_FLY_DEBUG << "erasing range " << *i;
|
|
i = m_spellCheckQueue.erase(i);
|
|
deleteMovingRangeQuickly(spellCheckRange);
|
|
}
|
|
else if(consideredRange.overlaps(spellCheckRange->toRange())) {
|
|
consideredRange.expandToRange(spellCheckRange->toRange());
|
|
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()) {
|
|
KateView *view = static_cast<KateView*>(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<KTextEditor::Range> rangesToReCheck;
|
|
for(QList<SpellCheckItem>::iterator i = m_spellCheckQueue.begin();
|
|
i != m_spellCheckQueue.end();) {
|
|
KTextEditor::MovingRange *spellCheckRange = (*i).first;
|
|
if(rangesAdjacent(spellCheckRange->toRange(), range) || spellCheckRange->contains(range)) {
|
|
ON_THE_FLY_DEBUG << "erasing range " << *i;
|
|
if(!spellCheckRange->isEmpty()) {
|
|
rangesToReCheck.push_back(spellCheckRange->toRange());
|
|
}
|
|
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->toRange())
|
|
&& (rangesAdjacent(spellCheckRange->toRange(), range) || spellCheckRange->contains(range))
|
|
&& !spellCheckRange->isEmpty()) {
|
|
rangesToReCheck.push_back(spellCheckRange->toRange());
|
|
ON_THE_FLY_DEBUG << "added the range " << *spellCheckRange;
|
|
stopCurrentSpellCheck();
|
|
deleteMovingRangeQuickly(spellCheckRange);
|
|
}
|
|
else if(spellCheckRange->isEmpty()) {
|
|
stopCurrentSpellCheck();
|
|
deleteMovingRangeQuickly(spellCheckRange);
|
|
}
|
|
else {
|
|
spellCheckInProgress = false;
|
|
}
|
|
}
|
|
for(QList<KTextEditor::Range>::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<KTextEditor::View*>& viewList = m_document->views();
|
|
for(QList<KTextEditor::View*>::const_iterator i = viewList.begin(); i != viewList.end(); ++i) {
|
|
KateView *view = static_cast<KateView*>(*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<SpellCheckItem>::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->toRange()); // make a copy!
|
|
deleteMovingRanges(highlightsList);
|
|
|
|
m_currentDecToEncOffsetList.clear();
|
|
KateDocument::OffsetList encToDecOffsetList;
|
|
QString text = m_document->decodeCharacters(spellCheckRange->toRange(),
|
|
m_currentDecToEncOffsetList,
|
|
encToDecOffsetList);
|
|
ON_THE_FLY_DEBUG << "next spell checking" << text;
|
|
if(text.isEmpty()) { // passing an empty string to speller can lead to a bad allocation exception
|
|
spellCheckDone(); // (bug 225867)
|
|
return;
|
|
}
|
|
if(!m_speller) {
|
|
updateConfig();
|
|
}
|
|
m_speller->setDictionary(language);
|
|
m_speller->setText(text);
|
|
m_speller->start();
|
|
}
|
|
|
|
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_speller) {
|
|
m_speller->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<SpellCheckItem>::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)
|
|
{
|
|
KateView *kateView = static_cast<KateView*>(view);
|
|
kateView->spellingMenu()->mouseEnteredMisspelledRange(range);
|
|
}
|
|
|
|
void KateOnTheFlyChecker::mouseExitedRange(KTextEditor::MovingRange *range, KTextEditor::View *view)
|
|
{
|
|
KateView *kateView = static_cast<KateView*>(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)
|
|
{
|
|
KateView *kateView = static_cast<KateView*>(view);
|
|
kateView->spellingMenu()->caretEnteredMisspelledRange(range);
|
|
}
|
|
|
|
void KateOnTheFlyChecker::caretExitedRange(KTextEditor::MovingRange *range, KTextEditor::View *view)
|
|
{
|
|
KateView *kateView = static_cast<KateView*>(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(NULL);
|
|
foreach(KTextEditor::View *view, m_document->views()) {
|
|
static_cast<KateView*>(view)->spellingMenu()->rangeDeleted(range);
|
|
}
|
|
delete(range);
|
|
}
|
|
|
|
void KateOnTheFlyChecker::deleteMovingRanges(const QList<KTextEditor::MovingRange*>& list)
|
|
{
|
|
foreach(KTextEditor::MovingRange *r, list) {
|
|
deleteMovingRange(r);
|
|
}
|
|
}
|
|
|
|
KTextEditor::Range KateOnTheFlyChecker::findWordBoundaries(const KTextEditor::Cursor& begin,
|
|
const KTextEditor::Cursor& end)
|
|
{
|
|
const QRegExp boundaryRegExp("\\b");
|
|
const QRegExp boundaryQuoteRegExp("\\b\\w+'\\w*$"); // handle spell checking of "isn't", "doesn't", etc.
|
|
const QRegExp extendedBoundaryRegExp("(\\W|$)");
|
|
const QRegExp extendedBoundaryQuoteRegExp("^\\w*'\\w+\\b"); // see above
|
|
KateDocument::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));
|
|
}
|
|
|
|
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<KTextEditor::MovingRange*> KateOnTheFlyChecker::installedMovingRanges(const KTextEditor::Range& range)
|
|
{
|
|
ON_THE_FLY_DEBUG << range;
|
|
MovingRangeList toReturn;
|
|
|
|
for(QList<SpellCheckItem>::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;
|
|
if(m_speller) {
|
|
delete m_speller;
|
|
m_speller = nullptr;
|
|
}
|
|
|
|
m_speller = new KSpeller(KGlobal::config().data(), this);
|
|
connect(m_speller, SIGNAL(misspelling(QString,int)), this, SLOT(misspelling(QString,int)));
|
|
connect(m_speller, SIGNAL(done()), this, SLOT(spellCheckDone()));
|
|
}
|
|
|
|
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(KateView*)), this, SLOT(restartViewRefreshTimer(KateView*)));
|
|
updateInstalledMovingRanges(static_cast<KateView*>(view));
|
|
}
|
|
|
|
void KateOnTheFlyChecker::viewDestroyed(QObject* obj)
|
|
{
|
|
ON_THE_FLY_DEBUG;
|
|
KTextEditor::View *view = static_cast<KTextEditor::View*>(obj);
|
|
m_displayRangeMap.remove(view);
|
|
}
|
|
|
|
void KateOnTheFlyChecker::removeView(KTextEditor::View *view)
|
|
{
|
|
ON_THE_FLY_DEBUG;
|
|
m_displayRangeMap.remove(view);
|
|
}
|
|
|
|
void KateOnTheFlyChecker::updateInstalledMovingRanges(KateView *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<KTextEditor::MovingRange*> 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()) {
|
|
KateView *view2 = static_cast<KateView*>(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()) {
|
|
KateView *view2 = static_cast<KateView*>(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<KTextEditor::View*>& viewList = m_document->views();
|
|
for(QList<KTextEditor::View*>::const_iterator i = viewList.begin(); i != viewList.end(); ++i) {
|
|
queueSpellCheckVisibleRange(static_cast<KateView*>(*i), range);
|
|
}
|
|
}
|
|
|
|
void KateOnTheFlyChecker::queueSpellCheckVisibleRange(KateView *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<QPair<KTextEditor::Range, QString> > spellCheckRanges
|
|
= KateGlobal::self()->spellCheckManager()->spellCheckRanges(m_document,
|
|
intersection,
|
|
true);
|
|
//we queue them up in reverse
|
|
QListIterator<QPair<KTextEditor::Range, QString> > i(spellCheckRanges);
|
|
i.toBack();
|
|
while(i.hasPrevious()) {
|
|
QPair<KTextEditor::Range, QString> p = i.previous();
|
|
queueLineSpellCheck(p.first, p.second);
|
|
}
|
|
}
|
|
|
|
void KateOnTheFlyChecker::queueLineSpellCheck(KateDocument *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<QPair<KTextEditor::Range, QString> > spellCheckRanges
|
|
= KateGlobal::self()->spellCheckManager()->spellCheckRanges(kateDocument,
|
|
range,
|
|
true);
|
|
//we queue them up in reverse
|
|
QListIterator<QPair<KTextEditor::Range, QString> > i(spellCheckRanges);
|
|
i.toBack();
|
|
while(i.hasPrevious()) {
|
|
QPair<KTextEditor::Range, QString> 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<SpellCheckItem>::iterator i = m_spellCheckQueue.begin();
|
|
i != m_spellCheckQueue.end();) {
|
|
KTextEditor::MovingRange *spellCheckRange = (*i).first;
|
|
if(range->contains(spellCheckRange->toRange())) {
|
|
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 = NULL;
|
|
}
|
|
|
|
void KateOnTheFlyChecker::restartViewRefreshTimer(KateView *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(NULL);
|
|
foreach(KTextEditor::View *view, m_document->views()) {
|
|
static_cast<KateView*>(view)->spellingMenu()->rangeDeleted(range);
|
|
}
|
|
delete(range);
|
|
}
|
|
|
|
void KateOnTheFlyChecker::handleModifiedRanges()
|
|
{
|
|
foreach(const ModificationItem &item, m_modificationList) {
|
|
KTextEditor::MovingRange *movingRange = item.second;
|
|
KTextEditor::Range range = movingRange->toRange();
|
|
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();
|
|
}
|
|
|
|
#include "moc_ontheflycheck.cpp"
|
|
|
|
// kate: space-indent on; indent-width 2; replace-tabs on;
|