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"