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
KateSpellCheckManager(QObject * parent)19 KateSpellCheckManager::KateSpellCheckManager(QObject *parent)
20 : QObject(parent)
21 {
22 }
23
24 KateSpellCheckManager::~KateSpellCheckManager() = default;
25
suggestions(const QString & word,const QString & dictionary)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
ignoreWord(const QString & word,const QString & dictionary)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
addToDictionary(const QString & word,const QString & dictionary)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
rangeDifference(const KTextEditor::Range & r1,const KTextEditor::Range & r2)49 QList<KTextEditor::Range> KateSpellCheckManager::rangeDifference(const KTextEditor::Range &r1, const 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(before);
57 }
58 if (!after.isEmpty()) {
59 toReturn.push_back(after);
60 }
61 return toReturn;
62 }
63
64 namespace
65 {
lessThanRangeDictionaryPair(const QPair<KTextEditor::Range,QString> & s1,const QPair<KTextEditor::Range,QString> & s2)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
spellCheckLanguageRanges(KTextEditor::DocumentPrivate * doc,const KTextEditor::Range & range)72 QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckLanguageRanges(KTextEditor::DocumentPrivate *doc, const 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(RangeDictionaryPair(range, defaultDict));
79 return toReturn;
80 }
81 QList<KTextEditor::Range> splitQueue;
82 splitQueue.push_back(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(consideredRange);
89 if (intersection.isEmpty()) {
90 continue;
91 }
92 toReturn.push_back(RangeDictionaryPair(intersection, (*i).second));
93 splitQueue += rangeDifference(consideredRange, 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(RangeDictionaryPair(consideredRange, defaultDict));
100 }
101 }
102 // finally, we still have to sort the list
103 std::stable_sort(toReturn.begin(), toReturn.end(), lessThanRangeDictionaryPair);
104 return toReturn;
105 }
106
spellCheckWrtHighlightingRanges(KTextEditor::DocumentPrivate * document,const KTextEditor::Range & range,const QString & dictionary,bool singleLine,bool returnSingleRange)107 QList<QPair<KTextEditor::Range, QString>> KateSpellCheckManager::spellCheckWrtHighlightingRanges(KTextEditor::DocumentPrivate *document,
108 const 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(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(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 Kate::TextLine kateTextLine = document->kateTextLine(line);
147 if (!kateTextLine) {
148 continue; // bug #303496
149 }
150 const int start = (line == startLine) ? startColumn : 0;
151 const int end = (line == endLine) ? endColumn : kateTextLine->length();
152 for (int i = start; i < end;) { // WARNING: 'i' has to be incremented manually!
153 int attr = kateTextLine->attribute(i);
154 const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr);
155 QString prefixFound = prefixStore.findPrefix(kateTextLine, i);
156 if (!document->highlight()->attributeRequiresSpellchecking(static_cast<unsigned int>(attr)) && prefixFound.isEmpty()) {
157 if (i == start) {
158 ++i;
159 continue;
160 } else if (inSpellCheckArea) {
161 KTextEditor::Range spellCheckRange(begin, KTextEditor::Cursor(line, i));
162 // work around Qt bug 6498
163 trimRange(document, spellCheckRange);
164 if (!spellCheckRange.isEmpty()) {
165 toReturn.push_back(RangeDictionaryPair(spellCheckRange, dictionary));
166 if (returnSingleRange) {
167 return toReturn;
168 }
169 }
170 begin = KTextEditor::Cursor::invalid();
171 inSpellCheckArea = false;
172 }
173 } else if (!inSpellCheckArea) {
174 begin = KTextEditor::Cursor(line, i);
175 inSpellCheckArea = true;
176 }
177 if (!prefixFound.isEmpty()) {
178 i += prefixFound.length();
179 } else {
180 ++i;
181 }
182 }
183 }
184 if (inSpellCheckArea) {
185 KTextEditor::Range spellCheckRange(begin, rangeToSplit.end());
186 // work around Qt bug 6498
187 trimRange(document, spellCheckRange);
188 if (!spellCheckRange.isEmpty()) {
189 toReturn.push_back(RangeDictionaryPair(spellCheckRange, dictionary));
190 if (returnSingleRange) {
191 return toReturn;
192 }
193 }
194 }
195 }
196
197 return toReturn;
198 }
199
200 QList<QPair<KTextEditor::Range, QString>>
spellCheckRanges(KTextEditor::DocumentPrivate * doc,const KTextEditor::Range & range,bool singleLine)201 KateSpellCheckManager::spellCheckRanges(KTextEditor::DocumentPrivate *doc, const KTextEditor::Range &range, bool singleLine)
202 {
203 QList<RangeDictionaryPair> toReturn;
204 QList<RangeDictionaryPair> languageRangeList = spellCheckLanguageRanges(doc, range);
205 for (QList<RangeDictionaryPair>::iterator i = languageRangeList.begin(); i != languageRangeList.end(); ++i) {
206 const RangeDictionaryPair &p = *i;
207 toReturn += spellCheckWrtHighlightingRanges(doc, p.first, p.second, singleLine);
208 }
209 return toReturn;
210 }
211
replaceCharactersEncodedIfNecessary(const QString & newWord,KTextEditor::DocumentPrivate * doc,const KTextEditor::Range & replacementRange)212 void KateSpellCheckManager::replaceCharactersEncodedIfNecessary(const QString &newWord,
213 KTextEditor::DocumentPrivate *doc,
214 const KTextEditor::Range &replacementRange)
215 {
216 const int attr = doc->kateTextLine(replacementRange.start().line())->attribute(replacementRange.start().column());
217 if (!doc->highlight()->getCharacterEncodings(attr).isEmpty() && doc->containsCharacterEncoding(replacementRange)) {
218 doc->replaceText(replacementRange, newWord);
219 doc->replaceCharactersByEncoding(KTextEditor::Range(replacementRange.start(), replacementRange.start() + KTextEditor::Cursor(0, newWord.length())));
220 } else {
221 doc->replaceText(replacementRange, newWord);
222 }
223 }
224
trimRange(KTextEditor::DocumentPrivate * doc,KTextEditor::Range & r)225 void KateSpellCheckManager::trimRange(KTextEditor::DocumentPrivate *doc, KTextEditor::Range &r)
226 {
227 if (r.isEmpty()) {
228 return;
229 }
230 KTextEditor::Cursor cursor = r.start();
231 while (cursor < r.end()) {
232 if (doc->lineLength(cursor.line()) > 0 && !doc->characterAt(cursor).isSpace() && doc->characterAt(cursor).category() != QChar::Other_Control) {
233 break;
234 }
235 cursor.setColumn(cursor.column() + 1);
236 if (cursor.column() >= doc->lineLength(cursor.line())) {
237 cursor.setPosition(cursor.line() + 1, 0);
238 }
239 }
240 r.setStart(cursor);
241 if (r.isEmpty()) {
242 return;
243 }
244
245 cursor = r.end();
246 KTextEditor::Cursor prevCursor = cursor;
247 // the range cannot be empty now
248 do {
249 prevCursor = cursor;
250 if (cursor.column() <= 0) {
251 cursor.setPosition(cursor.line() - 1, doc->lineLength(cursor.line() - 1));
252 } else {
253 cursor.setColumn(cursor.column() - 1);
254 }
255 if (cursor.column() < doc->lineLength(cursor.line()) && !doc->characterAt(cursor).isSpace()
256 && doc->characterAt(cursor).category() != QChar::Other_Control) {
257 break;
258 }
259 } while (cursor > r.start());
260 r.setEnd(prevCursor);
261 }
262