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"