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
19KateSpellCheckManager::KateSpellCheckManager(QObject *parent)
20 : QObject(parent)
21{
22}
23
24KateSpellCheckManager::~KateSpellCheckManager() = default;
25
26QStringList KateSpellCheckManager::suggestions(const QString &word, const QString &dictionary)
27{
28 Sonnet::Speller speller;
29 speller.setLanguage(dictionary);
30 return speller.suggest(word);
31}
32
33void 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
41void 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
49QList<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
64namespace
65{
66bool 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
72QList<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
107QList<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
197QList<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
208void 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
219void 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

source code of ktexteditor/src/spellcheck/spellcheck.cpp