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