File indexing completed on 2024-04-21 03:57:41

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             const auto kateTextLine = document->kateTextLine(line);
0147             const int start = (line == startLine) ? startColumn : 0;
0148             const int end = (line == endLine) ? endColumn : kateTextLine.length();
0149             for (int i = start; i < end;) { // WARNING: 'i' has to be incremented manually!
0150                 int attr = kateTextLine.attribute(i);
0151                 const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr);
0152                 QString prefixFound = prefixStore.findPrefix(kateTextLine, i);
0153                 if (!document->highlight()->attributeRequiresSpellchecking(static_cast<unsigned int>(attr)) && prefixFound.isEmpty()) {
0154                     if (i == start) {
0155                         ++i;
0156                         continue;
0157                     } else if (inSpellCheckArea) {
0158                         KTextEditor::Range spellCheckRange(begin, KTextEditor::Cursor(line, i));
0159                         // work around Qt bug 6498
0160                         trimRange(document, spellCheckRange);
0161                         if (!spellCheckRange.isEmpty()) {
0162                             toReturn.push_back(RangeDictionaryPair(spellCheckRange, dictionary));
0163                             if (returnSingleRange) {
0164                                 return toReturn;
0165                             }
0166                         }
0167                         begin = KTextEditor::Cursor::invalid();
0168                         inSpellCheckArea = false;
0169                     }
0170                 } else if (!inSpellCheckArea) {
0171                     begin = KTextEditor::Cursor(line, i);
0172                     inSpellCheckArea = true;
0173                 }
0174                 if (!prefixFound.isEmpty()) {
0175                     i += prefixFound.length();
0176                 } else {
0177                     ++i;
0178                 }
0179             }
0180         }
0181         if (inSpellCheckArea) {
0182             KTextEditor::Range spellCheckRange(begin, rangeToSplit.end());
0183             // work around Qt bug 6498
0184             trimRange(document, spellCheckRange);
0185             if (!spellCheckRange.isEmpty()) {
0186                 toReturn.push_back(RangeDictionaryPair(spellCheckRange, dictionary));
0187                 if (returnSingleRange) {
0188                     return toReturn;
0189                 }
0190             }
0191         }
0192     }
0193 
0194     return toReturn;
0195 }
0196 
0197 QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckRanges(KTextEditor::DocumentPrivate *doc, KTextEditor::Range range, bool singleLine)
0198 {
0199     QList<RangeDictionaryPair> toReturn;
0200     QList<RangeDictionaryPair> languageRangeList = spellCheckLanguageRanges(doc, range);
0201     for (QList<RangeDictionaryPair>::iterator i = languageRangeList.begin(); i != languageRangeList.end(); ++i) {
0202         const RangeDictionaryPair &p = *i;
0203         toReturn += spellCheckWrtHighlightingRanges(doc, p.first, p.second, singleLine);
0204     }
0205     return toReturn;
0206 }
0207 
0208 void KateSpellCheckManager::replaceCharactersEncodedIfNecessary(const QString &newWord, KTextEditor::DocumentPrivate *doc, KTextEditor::Range replacementRange)
0209 {
0210     const int attr = doc->kateTextLine(replacementRange.start().line()).attribute(replacementRange.start().column());
0211     if (!doc->highlight()->getCharacterEncodings(attr).isEmpty() && doc->containsCharacterEncoding(replacementRange)) {
0212         doc->replaceText(replacementRange, newWord);
0213         doc->replaceCharactersByEncoding(KTextEditor::Range(replacementRange.start(), replacementRange.start() + KTextEditor::Cursor(0, newWord.length())));
0214     } else {
0215         doc->replaceText(replacementRange, newWord);
0216     }
0217 }
0218 
0219 void KateSpellCheckManager::trimRange(KTextEditor::DocumentPrivate *doc, KTextEditor::Range &r)
0220 {
0221     if (r.isEmpty()) {
0222         return;
0223     }
0224     KTextEditor::Cursor cursor = r.start();
0225     while (cursor < r.end()) {
0226         if (doc->lineLength(cursor.line()) > 0 && !doc->characterAt(cursor).isSpace() && doc->characterAt(cursor).category() != QChar::Other_Control) {
0227             break;
0228         }
0229         cursor.setColumn(cursor.column() + 1);
0230         if (cursor.column() >= doc->lineLength(cursor.line())) {
0231             cursor.setPosition(cursor.line() + 1, 0);
0232         }
0233     }
0234     r.setStart(cursor);
0235     if (r.isEmpty()) {
0236         return;
0237     }
0238 
0239     cursor = r.end();
0240     KTextEditor::Cursor prevCursor = cursor;
0241     // the range cannot be empty now
0242     do {
0243         prevCursor = cursor;
0244         if (cursor.column() <= 0) {
0245             cursor.setPosition(cursor.line() - 1, doc->lineLength(cursor.line() - 1));
0246         } else {
0247             cursor.setColumn(cursor.column() - 1);
0248         }
0249         if (cursor.column() < doc->lineLength(cursor.line()) && !doc->characterAt(cursor).isSpace()
0250             && doc->characterAt(cursor).category() != QChar::Other_Control) {
0251             break;
0252         }
0253     } while (cursor > r.start());
0254     r.setEnd(prevCursor);
0255 }
0256 
0257 #include "moc_spellcheck.cpp"