| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2009 Michel Ludwig <michel.ludwig@kdemail.net> |
| 3 | |
| 4 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 5 | */ |
| 6 | |
| 7 | #include "spellcheck.h" |
| 8 | |
| 9 | #include <QHash> |
| 10 | #include <QTimer> |
| 11 | #include <QtAlgorithms> |
| 12 | |
| 13 | #include <KActionCollection> |
| 14 | #include <ktexteditor/view.h> |
| 15 | |
| 16 | #include "katedocument.h" |
| 17 | #include "katehighlight.h" |
| 18 | |
| 19 | KateSpellCheckManager::KateSpellCheckManager(QObject *parent) |
| 20 | : QObject(parent) |
| 21 | { |
| 22 | } |
| 23 | |
| 24 | KateSpellCheckManager::~KateSpellCheckManager() = default; |
| 25 | |
| 26 | QStringList KateSpellCheckManager::suggestions(const QString &word, const QString &dictionary) |
| 27 | { |
| 28 | Sonnet::Speller speller; |
| 29 | speller.setLanguage(dictionary); |
| 30 | return speller.suggest(word); |
| 31 | } |
| 32 | |
| 33 | void KateSpellCheckManager::ignoreWord(const QString &word, const QString &dictionary) |
| 34 | { |
| 35 | Sonnet::Speller speller; |
| 36 | speller.setLanguage(dictionary); |
| 37 | speller.addToSession(word); |
| 38 | Q_EMIT wordIgnored(word); |
| 39 | } |
| 40 | |
| 41 | void KateSpellCheckManager::addToDictionary(const QString &word, const QString &dictionary) |
| 42 | { |
| 43 | Sonnet::Speller speller; |
| 44 | speller.setLanguage(dictionary); |
| 45 | speller.addToPersonal(word); |
| 46 | Q_EMIT wordAddedToDictionary(word); |
| 47 | } |
| 48 | |
| 49 | QList<KTextEditor::Range> KateSpellCheckManager::rangeDifference(KTextEditor::Range r1, KTextEditor::Range r2) |
| 50 | { |
| 51 | Q_ASSERT(r1.contains(r2)); |
| 52 | QList<KTextEditor::Range> toReturn; |
| 53 | KTextEditor::Range before(r1.start(), r2.start()); |
| 54 | KTextEditor::Range after(r2.end(), r1.end()); |
| 55 | if (!before.isEmpty()) { |
| 56 | toReturn.push_back(t: before); |
| 57 | } |
| 58 | if (!after.isEmpty()) { |
| 59 | toReturn.push_back(t: after); |
| 60 | } |
| 61 | return toReturn; |
| 62 | } |
| 63 | |
| 64 | namespace |
| 65 | { |
| 66 | bool lessThanRangeDictionaryPair(const QPair<KTextEditor::Range, QString> &s1, const QPair<KTextEditor::Range, QString> &s2) |
| 67 | { |
| 68 | return s1.first.end() <= s2.first.start(); |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckLanguageRanges(KTextEditor::DocumentPrivate *doc, KTextEditor::Range range) |
| 73 | { |
| 74 | QString defaultDict = doc->defaultDictionary(); |
| 75 | QList<RangeDictionaryPair> toReturn; |
| 76 | QList<QPair<KTextEditor::MovingRange *, QString>> dictionaryRanges = doc->dictionaryRanges(); |
| 77 | if (dictionaryRanges.isEmpty()) { |
| 78 | toReturn.push_back(t: RangeDictionaryPair(range, defaultDict)); |
| 79 | return toReturn; |
| 80 | } |
| 81 | QList<KTextEditor::Range> splitQueue; |
| 82 | splitQueue.push_back(t: range); |
| 83 | while (!splitQueue.isEmpty()) { |
| 84 | bool handled = false; |
| 85 | KTextEditor::Range consideredRange = splitQueue.takeFirst(); |
| 86 | for (QList<QPair<KTextEditor::MovingRange *, QString>>::iterator i = dictionaryRanges.begin(); i != dictionaryRanges.end(); ++i) { |
| 87 | KTextEditor::Range languageRange = *((*i).first); |
| 88 | KTextEditor::Range intersection = languageRange.intersect(range: consideredRange); |
| 89 | if (intersection.isEmpty()) { |
| 90 | continue; |
| 91 | } |
| 92 | toReturn.push_back(t: RangeDictionaryPair(intersection, (*i).second)); |
| 93 | splitQueue += rangeDifference(r1: consideredRange, r2: intersection); |
| 94 | handled = true; |
| 95 | break; |
| 96 | } |
| 97 | if (!handled) { |
| 98 | // 'consideredRange' did not intersect with any dictionary range, so we add it with the default dictionary |
| 99 | toReturn.push_back(t: RangeDictionaryPair(consideredRange, defaultDict)); |
| 100 | } |
| 101 | } |
| 102 | // finally, we still have to sort the list |
| 103 | std::stable_sort(first: toReturn.begin(), last: toReturn.end(), comp: lessThanRangeDictionaryPair); |
| 104 | return toReturn; |
| 105 | } |
| 106 | |
| 107 | QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckWrtHighlightingRanges(KTextEditor::DocumentPrivate *document, |
| 108 | KTextEditor::Range range, |
| 109 | const QString &dictionary, |
| 110 | bool singleLine, |
| 111 | bool returnSingleRange) |
| 112 | { |
| 113 | QList<QPair<KTextEditor::Range, QString>> toReturn; |
| 114 | if (range.isEmpty()) { |
| 115 | return toReturn; |
| 116 | } |
| 117 | |
| 118 | KateHighlighting *highlighting = document->highlight(); |
| 119 | |
| 120 | QList<KTextEditor::Range> rangesToSplit; |
| 121 | if (!singleLine || range.onSingleLine()) { |
| 122 | rangesToSplit.push_back(t: range); |
| 123 | } else { |
| 124 | const int startLine = range.start().line(); |
| 125 | const int startColumn = range.start().column(); |
| 126 | const int endLine = range.end().line(); |
| 127 | const int endColumn = range.end().column(); |
| 128 | for (int line = startLine; line <= endLine; ++line) { |
| 129 | const int start = (line == startLine) ? startColumn : 0; |
| 130 | const int end = (line == endLine) ? endColumn : document->lineLength(line); |
| 131 | KTextEditor::Range toAdd(line, start, line, end); |
| 132 | if (!toAdd.isEmpty()) { |
| 133 | rangesToSplit.push_back(t: toAdd); |
| 134 | } |
| 135 | } |
| 136 | } |
| 137 | for (QList<KTextEditor::Range>::iterator i = rangesToSplit.begin(); i != rangesToSplit.end(); ++i) { |
| 138 | KTextEditor::Range rangeToSplit = *i; |
| 139 | KTextEditor::Cursor begin = KTextEditor::Cursor::invalid(); |
| 140 | const int startLine = rangeToSplit.start().line(); |
| 141 | const int startColumn = rangeToSplit.start().column(); |
| 142 | const int endLine = rangeToSplit.end().line(); |
| 143 | const int endColumn = rangeToSplit.end().column(); |
| 144 | bool inSpellCheckArea = false; |
| 145 | for (int line = startLine; line <= endLine; ++line) { |
| 146 | const auto kateTextLine = document->kateTextLine(i: line); |
| 147 | const int start = (line == startLine) ? startColumn : 0; |
| 148 | const int end = (line == endLine) ? endColumn : kateTextLine.length(); |
| 149 | for (int i = start; i < end;) { // WARNING: 'i' has to be incremented manually! |
| 150 | int attr = kateTextLine.attribute(pos: i); |
| 151 | const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attrib: attr); |
| 152 | QString prefixFound = prefixStore.findPrefix(line: kateTextLine, start: i); |
| 153 | if (!document->highlight()->attributeRequiresSpellchecking(attr: static_cast<unsigned int>(attr)) && prefixFound.isEmpty()) { |
| 154 | if (i == start) { |
| 155 | ++i; |
| 156 | continue; |
| 157 | } else if (inSpellCheckArea) { |
| 158 | KTextEditor::Range spellCheckRange(begin, KTextEditor::Cursor(line, i)); |
| 159 | // work around Qt bug 6498 |
| 160 | trimRange(doc: document, r&: spellCheckRange); |
| 161 | if (!spellCheckRange.isEmpty()) { |
| 162 | toReturn.push_back(t: RangeDictionaryPair(spellCheckRange, dictionary)); |
| 163 | if (returnSingleRange) { |
| 164 | return toReturn; |
| 165 | } |
| 166 | } |
| 167 | begin = KTextEditor::Cursor::invalid(); |
| 168 | inSpellCheckArea = false; |
| 169 | } |
| 170 | } else if (!inSpellCheckArea) { |
| 171 | begin = KTextEditor::Cursor(line, i); |
| 172 | inSpellCheckArea = true; |
| 173 | } |
| 174 | if (!prefixFound.isEmpty()) { |
| 175 | i += prefixFound.length(); |
| 176 | } else { |
| 177 | ++i; |
| 178 | } |
| 179 | } |
| 180 | } |
| 181 | if (inSpellCheckArea) { |
| 182 | KTextEditor::Range spellCheckRange(begin, rangeToSplit.end()); |
| 183 | // work around Qt bug 6498 |
| 184 | trimRange(doc: document, r&: spellCheckRange); |
| 185 | if (!spellCheckRange.isEmpty()) { |
| 186 | toReturn.push_back(t: RangeDictionaryPair(spellCheckRange, dictionary)); |
| 187 | if (returnSingleRange) { |
| 188 | return toReturn; |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | return toReturn; |
| 195 | } |
| 196 | |
| 197 | QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckRanges(KTextEditor::DocumentPrivate *doc, KTextEditor::Range range, bool singleLine) |
| 198 | { |
| 199 | QList<RangeDictionaryPair> toReturn; |
| 200 | QList<RangeDictionaryPair> languageRangeList = spellCheckLanguageRanges(doc, range); |
| 201 | for (QList<RangeDictionaryPair>::iterator i = languageRangeList.begin(); i != languageRangeList.end(); ++i) { |
| 202 | const RangeDictionaryPair &p = *i; |
| 203 | toReturn += spellCheckWrtHighlightingRanges(document: doc, range: p.first, dictionary: p.second, singleLine); |
| 204 | } |
| 205 | return toReturn; |
| 206 | } |
| 207 | |
| 208 | void KateSpellCheckManager::replaceCharactersEncodedIfNecessary(const QString &newWord, KTextEditor::DocumentPrivate *doc, KTextEditor::Range replacementRange) |
| 209 | { |
| 210 | const int attr = doc->kateTextLine(i: replacementRange.start().line()).attribute(pos: replacementRange.start().column()); |
| 211 | if (!doc->highlight()->getCharacterEncodings(attrib: attr).isEmpty() && doc->containsCharacterEncoding(range: replacementRange)) { |
| 212 | doc->replaceText(range: replacementRange, s: newWord); |
| 213 | doc->replaceCharactersByEncoding(range: KTextEditor::Range(replacementRange.start(), replacementRange.start() + KTextEditor::Cursor(0, newWord.length()))); |
| 214 | } else { |
| 215 | doc->replaceText(range: replacementRange, s: newWord); |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | void KateSpellCheckManager::trimRange(KTextEditor::DocumentPrivate *doc, KTextEditor::Range &r) |
| 220 | { |
| 221 | if (r.isEmpty()) { |
| 222 | return; |
| 223 | } |
| 224 | KTextEditor::Cursor cursor = r.start(); |
| 225 | while (cursor < r.end()) { |
| 226 | if (doc->lineLength(line: cursor.line()) > 0 && !doc->characterAt(position: cursor).isSpace() && doc->characterAt(position: cursor).category() != QChar::Other_Control) { |
| 227 | break; |
| 228 | } |
| 229 | cursor.setColumn(cursor.column() + 1); |
| 230 | if (cursor.column() >= doc->lineLength(line: cursor.line())) { |
| 231 | cursor.setPosition(line: cursor.line() + 1, column: 0); |
| 232 | } |
| 233 | } |
| 234 | r.setStart(cursor); |
| 235 | if (r.isEmpty()) { |
| 236 | return; |
| 237 | } |
| 238 | |
| 239 | cursor = r.end(); |
| 240 | KTextEditor::Cursor prevCursor = cursor; |
| 241 | // the range cannot be empty now |
| 242 | do { |
| 243 | prevCursor = cursor; |
| 244 | if (cursor.column() <= 0) { |
| 245 | cursor.setPosition(line: cursor.line() - 1, column: doc->lineLength(line: cursor.line() - 1)); |
| 246 | } else { |
| 247 | cursor.setColumn(cursor.column() - 1); |
| 248 | } |
| 249 | if (cursor.column() < doc->lineLength(line: cursor.line()) && !doc->characterAt(position: cursor).isSpace() |
| 250 | && doc->characterAt(position: cursor).category() != QChar::Other_Control) { |
| 251 | break; |
| 252 | } |
| 253 | } while (cursor > r.start()); |
| 254 | r.setEnd(prevCursor); |
| 255 | } |
| 256 | |
| 257 | #include "moc_spellcheck.cpp" |
| 258 | |