File indexing completed on 2024-04-28 15:30:45

0001 /*
0002     SPDX-FileCopyrightText: 2009 Michel Ludwig <michel.ludwig@kdemail.net>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "spellcheck.h"
0008 
0009 #include <QHash>
0010 #include <QTimer>
0011 #include <QtAlgorithms>
0012 
0013 #include <KActionCollection>
0014 #include <ktexteditor/view.h>
0015 
0016 #include "katedocument.h"
0017 #include "katehighlight.h"
0018 
0019 KateSpellCheckManager::KateSpellCheckManager(QObject *parent)
0020     : QObject(parent)
0021 {
0022 }
0023 
0024 KateSpellCheckManager::~KateSpellCheckManager() = default;
0025 
0026 QStringList KateSpellCheckManager::suggestions(const QString &word, const QString &dictionary)
0027 {
0028     Sonnet::Speller speller;
0029     speller.setLanguage(dictionary);
0030     return speller.suggest(word);
0031 }
0032 
0033 void KateSpellCheckManager::ignoreWord(const QString &word, const QString &dictionary)
0034 {
0035     Sonnet::Speller speller;
0036     speller.setLanguage(dictionary);
0037     speller.addToSession(word);
0038     Q_EMIT wordIgnored(word);
0039 }
0040 
0041 void KateSpellCheckManager::addToDictionary(const QString &word, const QString &dictionary)
0042 {
0043     Sonnet::Speller speller;
0044     speller.setLanguage(dictionary);
0045     speller.addToPersonal(word);
0046     Q_EMIT wordAddedToDictionary(word);
0047 }
0048 
0049 QList<KTextEditor::Range> KateSpellCheckManager::rangeDifference(KTextEditor::Range r1, KTextEditor::Range r2)
0050 {
0051     Q_ASSERT(r1.contains(r2));
0052     QList<KTextEditor::Range> toReturn;
0053     KTextEditor::Range before(r1.start(), r2.start());
0054     KTextEditor::Range after(r2.end(), r1.end());
0055     if (!before.isEmpty()) {
0056         toReturn.push_back(before);
0057     }
0058     if (!after.isEmpty()) {
0059         toReturn.push_back(after);
0060     }
0061     return toReturn;
0062 }
0063 
0064 namespace
0065 {
0066 bool lessThanRangeDictionaryPair(const QPair<KTextEditor::Range, QString> &s1, const QPair<KTextEditor::Range, QString> &s2)
0067 {
0068     return s1.first.end() <= s2.first.start();
0069 }
0070 }
0071 
0072 QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckLanguageRanges(KTextEditor::DocumentPrivate *doc, KTextEditor::Range range)
0073 {
0074     QString defaultDict = doc->defaultDictionary();
0075     QList<RangeDictionaryPair> toReturn;
0076     QList<QPair<KTextEditor::MovingRange *, QString>> dictionaryRanges = doc->dictionaryRanges();
0077     if (dictionaryRanges.isEmpty()) {
0078         toReturn.push_back(RangeDictionaryPair(range, defaultDict));
0079         return toReturn;
0080     }
0081     QList<KTextEditor::Range> splitQueue;
0082     splitQueue.push_back(range);
0083     while (!splitQueue.isEmpty()) {
0084         bool handled = false;
0085         KTextEditor::Range consideredRange = splitQueue.takeFirst();
0086         for (QList<QPair<KTextEditor::MovingRange *, QString>>::iterator i = dictionaryRanges.begin(); i != dictionaryRanges.end(); ++i) {
0087             KTextEditor::Range languageRange = *((*i).first);
0088             KTextEditor::Range intersection = languageRange.intersect(consideredRange);
0089             if (intersection.isEmpty()) {
0090                 continue;
0091             }
0092             toReturn.push_back(RangeDictionaryPair(intersection, (*i).second));
0093             splitQueue += rangeDifference(consideredRange, intersection);
0094             handled = true;
0095             break;
0096         }
0097         if (!handled) {
0098             // 'consideredRange' did not intersect with any dictionary range, so we add it with the default dictionary
0099             toReturn.push_back(RangeDictionaryPair(consideredRange, defaultDict));
0100         }
0101     }
0102     // finally, we still have to sort the list
0103     std::stable_sort(toReturn.begin(), toReturn.end(), lessThanRangeDictionaryPair);
0104     return toReturn;
0105 }
0106 
0107 QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckWrtHighlightingRanges(KTextEditor::DocumentPrivate *document,
0108                                                                                                  KTextEditor::Range range,
0109                                                                                                  const QString &dictionary,
0110                                                                                                  bool singleLine,
0111                                                                                                  bool returnSingleRange)
0112 {
0113     QList<QPair<KTextEditor::Range, QString>> toReturn;
0114     if (range.isEmpty()) {
0115         return toReturn;
0116     }
0117 
0118     KateHighlighting *highlighting = document->highlight();
0119 
0120     QList<KTextEditor::Range> rangesToSplit;
0121     if (!singleLine || range.onSingleLine()) {
0122         rangesToSplit.push_back(range);
0123     } else {
0124         const int startLine = range.start().line();
0125         const int startColumn = range.start().column();
0126         const int endLine = range.end().line();
0127         const int endColumn = range.end().column();
0128         for (int line = startLine; line <= endLine; ++line) {
0129             const int start = (line == startLine) ? startColumn : 0;
0130             const int end = (line == endLine) ? endColumn : document->lineLength(line);
0131             KTextEditor::Range toAdd(line, start, line, end);
0132             if (!toAdd.isEmpty()) {
0133                 rangesToSplit.push_back(toAdd);
0134             }
0135         }
0136     }
0137     for (QList<KTextEditor::Range>::iterator i = rangesToSplit.begin(); i != rangesToSplit.end(); ++i) {
0138         KTextEditor::Range rangeToSplit = *i;
0139         KTextEditor::Cursor begin = KTextEditor::Cursor::invalid();
0140         const int startLine = rangeToSplit.start().line();
0141         const int startColumn = rangeToSplit.start().column();
0142         const int endLine = rangeToSplit.end().line();
0143         const int endColumn = rangeToSplit.end().column();
0144         bool inSpellCheckArea = false;
0145         for (int line = startLine; line <= endLine; ++line) {
0146             Kate::TextLine kateTextLine = document->kateTextLine(line);
0147             if (!kateTextLine) {
0148                 continue; // bug #303496
0149             }
0150             const int start = (line == startLine) ? startColumn : 0;
0151             const int end = (line == endLine) ? endColumn : kateTextLine->length();
0152             for (int i = start; i < end;) { // WARNING: 'i' has to be incremented manually!
0153                 int attr = kateTextLine->attribute(i);
0154                 const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr);
0155                 QString prefixFound = prefixStore.findPrefix(kateTextLine, i);
0156                 if (!document->highlight()->attributeRequiresSpellchecking(static_cast<unsigned int>(attr)) && prefixFound.isEmpty()) {
0157                     if (i == start) {
0158                         ++i;
0159                         continue;
0160                     } else if (inSpellCheckArea) {
0161                         KTextEditor::Range spellCheckRange(begin, KTextEditor::Cursor(line, i));
0162                         // work around Qt bug 6498
0163                         trimRange(document, spellCheckRange);
0164                         if (!spellCheckRange.isEmpty()) {
0165                             toReturn.push_back(RangeDictionaryPair(spellCheckRange, dictionary));
0166                             if (returnSingleRange) {
0167                                 return toReturn;
0168                             }
0169                         }
0170                         begin = KTextEditor::Cursor::invalid();
0171                         inSpellCheckArea = false;
0172                     }
0173                 } else if (!inSpellCheckArea) {
0174                     begin = KTextEditor::Cursor(line, i);
0175                     inSpellCheckArea = true;
0176                 }
0177                 if (!prefixFound.isEmpty()) {
0178                     i += prefixFound.length();
0179                 } else {
0180                     ++i;
0181                 }
0182             }
0183         }
0184         if (inSpellCheckArea) {
0185             KTextEditor::Range spellCheckRange(begin, rangeToSplit.end());
0186             // work around Qt bug 6498
0187             trimRange(document, spellCheckRange);
0188             if (!spellCheckRange.isEmpty()) {
0189                 toReturn.push_back(RangeDictionaryPair(spellCheckRange, dictionary));
0190                 if (returnSingleRange) {
0191                     return toReturn;
0192                 }
0193             }
0194         }
0195     }
0196 
0197     return toReturn;
0198 }
0199 
0200 QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckRanges(KTextEditor::DocumentPrivate *doc, KTextEditor::Range range, bool singleLine)
0201 {
0202     QList<RangeDictionaryPair> toReturn;
0203     QList<RangeDictionaryPair> languageRangeList = spellCheckLanguageRanges(doc, range);
0204     for (QList<RangeDictionaryPair>::iterator i = languageRangeList.begin(); i != languageRangeList.end(); ++i) {
0205         const RangeDictionaryPair &p = *i;
0206         toReturn += spellCheckWrtHighlightingRanges(doc, p.first, p.second, singleLine);
0207     }
0208     return toReturn;
0209 }
0210 
0211 void KateSpellCheckManager::replaceCharactersEncodedIfNecessary(const QString &newWord, KTextEditor::DocumentPrivate *doc, KTextEditor::Range replacementRange)
0212 {
0213     const int attr = doc->kateTextLine(replacementRange.start().line())->attribute(replacementRange.start().column());
0214     if (!doc->highlight()->getCharacterEncodings(attr).isEmpty() && doc->containsCharacterEncoding(replacementRange)) {
0215         doc->replaceText(replacementRange, newWord);
0216         doc->replaceCharactersByEncoding(KTextEditor::Range(replacementRange.start(), replacementRange.start() + KTextEditor::Cursor(0, newWord.length())));
0217     } else {
0218         doc->replaceText(replacementRange, newWord);
0219     }
0220 }
0221 
0222 void KateSpellCheckManager::trimRange(KTextEditor::DocumentPrivate *doc, KTextEditor::Range &r)
0223 {
0224     if (r.isEmpty()) {
0225         return;
0226     }
0227     KTextEditor::Cursor cursor = r.start();
0228     while (cursor < r.end()) {
0229         if (doc->lineLength(cursor.line()) > 0 && !doc->characterAt(cursor).isSpace() && doc->characterAt(cursor).category() != QChar::Other_Control) {
0230             break;
0231         }
0232         cursor.setColumn(cursor.column() + 1);
0233         if (cursor.column() >= doc->lineLength(cursor.line())) {
0234             cursor.setPosition(cursor.line() + 1, 0);
0235         }
0236     }
0237     r.setStart(cursor);
0238     if (r.isEmpty()) {
0239         return;
0240     }
0241 
0242     cursor = r.end();
0243     KTextEditor::Cursor prevCursor = cursor;
0244     // the range cannot be empty now
0245     do {
0246         prevCursor = cursor;
0247         if (cursor.column() <= 0) {
0248             cursor.setPosition(cursor.line() - 1, doc->lineLength(cursor.line() - 1));
0249         } else {
0250             cursor.setColumn(cursor.column() - 1);
0251         }
0252         if (cursor.column() < doc->lineLength(cursor.line()) && !doc->characterAt(cursor).isSpace()
0253             && doc->characterAt(cursor).category() != QChar::Other_Control) {
0254             break;
0255         }
0256     } while (cursor > r.start());
0257     r.setEnd(prevCursor);
0258 }
0259 
0260 #include "moc_spellcheck.cpp"